1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.textclassifier;
18 
19 import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_OTHERS;
20 import static android.view.textclassifier.ConversationActions.Message.PERSON_USER_SELF;
21 import static com.google.common.truth.Truth.assertThat;
22 
23 import android.app.PendingIntent;
24 import android.app.Person;
25 import android.app.RemoteAction;
26 import android.content.ComponentName;
27 import android.content.Intent;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.ResolveInfo;
30 import android.graphics.drawable.Icon;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.view.textclassifier.ConversationAction;
34 import android.view.textclassifier.ConversationActions;
35 import androidx.test.core.app.ApplicationProvider;
36 import androidx.test.ext.junit.runners.AndroidJUnit4;
37 import androidx.test.filters.SmallTest;
38 import com.android.textclassifier.common.intent.LabeledIntent;
39 import com.android.textclassifier.common.intent.LabeledIntent.TitleChooser;
40 import com.android.textclassifier.common.intent.TemplateIntentFactory;
41 import com.google.android.textclassifier.ActionsSuggestionsModel;
42 import com.google.android.textclassifier.RemoteActionTemplate;
43 import com.google.common.collect.ImmutableList;
44 import java.time.Instant;
45 import java.time.ZoneId;
46 import java.time.ZonedDateTime;
47 import java.util.Arrays;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.Locale;
51 import java.util.function.Function;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 @SmallTest
56 @RunWith(AndroidJUnit4.class)
57 public class ActionsSuggestionsHelperTest {
58   private static final String LOCALE_TAG = Locale.US.toLanguageTag();
59   private static final Function<CharSequence, List<String>> LANGUAGE_DETECTOR =
60       charSequence -> Collections.singletonList(LOCALE_TAG);
61 
62   @Test
testToNativeMessages_emptyInput()63   public void testToNativeMessages_emptyInput() {
64     ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
65         ActionsSuggestionsHelper.toNativeMessages(ImmutableList.of(), LANGUAGE_DETECTOR);
66 
67     assertThat(conversationMessages).isEmpty();
68   }
69 
70   @Test
testToNativeMessages_noTextMessages()71   public void testToNativeMessages_noTextMessages() {
72     ConversationActions.Message messageWithoutText =
73         new ConversationActions.Message.Builder(PERSON_USER_OTHERS).build();
74 
75     ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
76         ActionsSuggestionsHelper.toNativeMessages(
77             ImmutableList.of(messageWithoutText), LANGUAGE_DETECTOR);
78 
79     assertThat(conversationMessages).isEmpty();
80   }
81 
82   @Test
testToNativeMessages_userIdEncoding()83   public void testToNativeMessages_userIdEncoding() {
84     Person.Builder userA = new Person.Builder().setName("userA").setKey("A");
85     Person.Builder userB = new Person.Builder().setName("userB").setKey("B");
86 
87     ConversationActions.Message firstMessage =
88         new ConversationActions.Message.Builder(userB.build()).setText("first").build();
89     ConversationActions.Message secondMessage =
90         new ConversationActions.Message.Builder(userA.build()).setText("second").build();
91     ConversationActions.Message thirdMessage =
92         new ConversationActions.Message.Builder(PERSON_USER_SELF).setText("third").build();
93     ConversationActions.Message fourthMessage =
94         new ConversationActions.Message.Builder(userA.build()).setText("fourth").build();
95 
96     ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
97         ActionsSuggestionsHelper.toNativeMessages(
98             Arrays.asList(firstMessage, secondMessage, thirdMessage, fourthMessage),
99             LANGUAGE_DETECTOR);
100 
101     assertThat(conversationMessages).hasLength(4);
102     assertNativeMessage(conversationMessages[0], firstMessage.getText(), 2, 0);
103     assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
104     assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 0, 0);
105     assertNativeMessage(conversationMessages[3], fourthMessage.getText(), 1, 0);
106   }
107 
108   @Test
testToNativeMessages_referenceTime()109   public void testToNativeMessages_referenceTime() {
110     ConversationActions.Message firstMessage =
111         new ConversationActions.Message.Builder(PERSON_USER_OTHERS)
112             .setText("first")
113             .setReferenceTime(createZonedDateTimeFromMsUtc(1000))
114             .build();
115     ConversationActions.Message secondMessage =
116         new ConversationActions.Message.Builder(PERSON_USER_OTHERS).setText("second").build();
117     ConversationActions.Message thirdMessage =
118         new ConversationActions.Message.Builder(PERSON_USER_OTHERS)
119             .setText("third")
120             .setReferenceTime(createZonedDateTimeFromMsUtc(2000))
121             .build();
122 
123     ActionsSuggestionsModel.ConversationMessage[] conversationMessages =
124         ActionsSuggestionsHelper.toNativeMessages(
125             Arrays.asList(firstMessage, secondMessage, thirdMessage), LANGUAGE_DETECTOR);
126 
127     assertThat(conversationMessages).hasLength(3);
128     assertNativeMessage(conversationMessages[0], firstMessage.getText(), 1, 1000);
129     assertNativeMessage(conversationMessages[1], secondMessage.getText(), 1, 0);
130     assertNativeMessage(conversationMessages[2], thirdMessage.getText(), 1, 2000);
131   }
132 
133   @Test
testDeduplicateActions()134   public void testDeduplicateActions() {
135     Bundle phoneExtras = new Bundle();
136     Intent phoneIntent = new Intent();
137     phoneIntent.setComponent(new ComponentName("phone", "intent"));
138     ExtrasUtils.putActionIntent(phoneExtras, phoneIntent);
139 
140     Bundle anotherPhoneExtras = new Bundle();
141     Intent anotherPhoneIntent = new Intent();
142     anotherPhoneIntent.setComponent(new ComponentName("phone", "another.intent"));
143     ExtrasUtils.putActionIntent(anotherPhoneExtras, anotherPhoneIntent);
144 
145     Bundle urlExtras = new Bundle();
146     Intent urlIntent = new Intent();
147     urlIntent.setComponent(new ComponentName("url", "intent"));
148     ExtrasUtils.putActionIntent(urlExtras, urlIntent);
149 
150     PendingIntent pendingIntent =
151         PendingIntent.getActivity(ApplicationProvider.getApplicationContext(), 0, phoneIntent, 0);
152     Icon icon = Icon.createWithData(new byte[0], 0, 0);
153     ConversationAction action =
154         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
155             .setAction(new RemoteAction(icon, "label", "1", pendingIntent))
156             .setExtras(phoneExtras)
157             .build();
158     ConversationAction actionWithSameLabel =
159         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
160             .setAction(new RemoteAction(icon, "label", "2", pendingIntent))
161             .setExtras(phoneExtras)
162             .build();
163     ConversationAction actionWithSamePackageButDifferentClass =
164         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
165             .setAction(new RemoteAction(icon, "label", "3", pendingIntent))
166             .setExtras(anotherPhoneExtras)
167             .build();
168     ConversationAction actionWithDifferentLabel =
169         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
170             .setAction(new RemoteAction(icon, "another_label", "4", pendingIntent))
171             .setExtras(phoneExtras)
172             .build();
173     ConversationAction actionWithDifferentPackage =
174         new ConversationAction.Builder(ConversationAction.TYPE_OPEN_URL)
175             .setAction(new RemoteAction(icon, "label", "5", pendingIntent))
176             .setExtras(urlExtras)
177             .build();
178     ConversationAction actionWithoutRemoteAction =
179         new ConversationAction.Builder(ConversationAction.TYPE_CREATE_REMINDER).build();
180 
181     List<ConversationAction> conversationActions =
182         ActionsSuggestionsHelper.removeActionsWithDuplicates(
183             Arrays.asList(
184                 action,
185                 actionWithSameLabel,
186                 actionWithSamePackageButDifferentClass,
187                 actionWithDifferentLabel,
188                 actionWithDifferentPackage,
189                 actionWithoutRemoteAction));
190 
191     assertThat(conversationActions).hasSize(3);
192     assertThat(conversationActions.get(0).getAction().getContentDescription().toString())
193         .isEqualTo("4");
194     assertThat(conversationActions.get(1).getAction().getContentDescription().toString())
195         .isEqualTo("5");
196     assertThat(conversationActions.get(2).getAction()).isNull();
197   }
198 
199   @Test
testDeduplicateActions_nullComponent()200   public void testDeduplicateActions_nullComponent() {
201     Bundle phoneExtras = new Bundle();
202     Intent phoneIntent = new Intent(Intent.ACTION_DIAL);
203     ExtrasUtils.putActionIntent(phoneExtras, phoneIntent);
204     PendingIntent pendingIntent =
205         PendingIntent.getActivity(ApplicationProvider.getApplicationContext(), 0, phoneIntent, 0);
206     Icon icon = Icon.createWithData(new byte[0], 0, 0);
207     ConversationAction action =
208         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
209             .setAction(new RemoteAction(icon, "label", "1", pendingIntent))
210             .setExtras(phoneExtras)
211             .build();
212     ConversationAction actionWithSameLabel =
213         new ConversationAction.Builder(ConversationAction.TYPE_CALL_PHONE)
214             .setAction(new RemoteAction(icon, "label", "2", pendingIntent))
215             .setExtras(phoneExtras)
216             .build();
217 
218     List<ConversationAction> conversationActions =
219         ActionsSuggestionsHelper.removeActionsWithDuplicates(
220             Arrays.asList(action, actionWithSameLabel));
221 
222     assertThat(conversationActions).isEmpty();
223   }
224 
225   @Test
createLabeledIntentResult_null()226   public void createLabeledIntentResult_null() {
227     ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
228         new ActionsSuggestionsModel.ActionSuggestion(
229             "text", ConversationAction.TYPE_OPEN_URL, 1.0f, null, null, null, null);
230 
231     LabeledIntent.Result labeledIntentResult =
232         ActionsSuggestionsHelper.createLabeledIntentResult(
233             ApplicationProvider.getApplicationContext(),
234             new TemplateIntentFactory(),
235             nativeSuggestion);
236 
237     assertThat(labeledIntentResult).isNull();
238   }
239 
240   @Test
createLabeledIntentResult_emptyList()241   public void createLabeledIntentResult_emptyList() {
242     ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
243         new ActionsSuggestionsModel.ActionSuggestion(
244             "text",
245             ConversationAction.TYPE_OPEN_URL,
246             1.0f,
247             null,
248             null,
249             new RemoteActionTemplate[0],
250             null);
251 
252     LabeledIntent.Result labeledIntentResult =
253         ActionsSuggestionsHelper.createLabeledIntentResult(
254             ApplicationProvider.getApplicationContext(),
255             new TemplateIntentFactory(),
256             nativeSuggestion);
257 
258     assertThat(labeledIntentResult).isNull();
259   }
260 
261   @Test
createLabeledIntentResult()262   public void createLabeledIntentResult() {
263     ActionsSuggestionsModel.ActionSuggestion nativeSuggestion =
264         new ActionsSuggestionsModel.ActionSuggestion(
265             "text",
266             ConversationAction.TYPE_OPEN_URL,
267             1.0f,
268             null,
269             null,
270             new RemoteActionTemplate[] {
271               new RemoteActionTemplate(
272                   "title",
273                   null,
274                   "description",
275                   null,
276                   Intent.ACTION_VIEW,
277                   Uri.parse("http://www.android.com").toString(),
278                   null,
279                   0,
280                   null,
281                   null,
282                   null,
283                   0)
284             },
285             null);
286 
287     LabeledIntent.Result labeledIntentResult =
288         ActionsSuggestionsHelper.createLabeledIntentResult(
289             ApplicationProvider.getApplicationContext(),
290             new TemplateIntentFactory(),
291             nativeSuggestion);
292 
293     assertThat(labeledIntentResult.remoteAction.getTitle().toString()).isEqualTo("title");
294     assertThat(labeledIntentResult.resolvedIntent.getAction()).isEqualTo(Intent.ACTION_VIEW);
295   }
296 
297   @Test
createTitleChooser_notOpenUrl()298   public void createTitleChooser_notOpenUrl() {
299     assertThat(ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_CALL_PHONE))
300         .isNull();
301   }
302 
303   @Test
createTitleChooser_openUrl_resolveInfoIsNull()304   public void createTitleChooser_openUrl_resolveInfoIsNull() {
305     TitleChooser titleChooser =
306         ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_OPEN_URL);
307     LabeledIntent labeledIntent = createWebLabeledIntent();
308 
309     assertThat(titleChooser.chooseTitle(labeledIntent, /* resolveInfo= */ null).toString())
310         .isEqualTo("titleWithEntity");
311   }
312 
313   @Test
createTitleChooser_openUrl_packageIsNotAndroidAndHandleAllWebDataUriTrue()314   public void createTitleChooser_openUrl_packageIsNotAndroidAndHandleAllWebDataUriTrue() {
315     TitleChooser titleChooser =
316         ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_OPEN_URL);
317     LabeledIntent labeledIntent = createWebLabeledIntent();
318 
319     assertThat(
320             titleChooser
321                 .chooseTitle(
322                     labeledIntent,
323                     createResolveInfo("com.android.chrome", /* handleAllWebDataURI= */ true))
324                 .toString())
325         .isEqualTo("titleWithEntity");
326   }
327 
328   @Test
createTitleChooser_openUrl_packageIsNotAndroidAndHandleAllWebDataUriFalse()329   public void createTitleChooser_openUrl_packageIsNotAndroidAndHandleAllWebDataUriFalse() {
330     TitleChooser titleChooser =
331         ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_OPEN_URL);
332     LabeledIntent labeledIntent = createWebLabeledIntent();
333 
334     assertThat(
335             titleChooser
336                 .chooseTitle(
337                     labeledIntent,
338                     createResolveInfo("com.youtube", /* handleAllWebDataURI= */ false))
339                 .toString())
340         .isEqualTo("titleWithoutEntity");
341   }
342 
343   @Test
createTitleChooser_openUrl_packageIsAndroidAndHandleAllWebDataUriFalse()344   public void createTitleChooser_openUrl_packageIsAndroidAndHandleAllWebDataUriFalse() {
345     TitleChooser titleChooser =
346         ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_OPEN_URL);
347     LabeledIntent labeledIntent = createWebLabeledIntent();
348 
349     assertThat(
350             titleChooser
351                 .chooseTitle(
352                     labeledIntent, createResolveInfo("android", /* handleAllWebDataURI= */ false))
353                 .toString())
354         .isEqualTo("titleWithEntity");
355   }
356 
357   @Test
createTitleChooser_openUrl_packageIsAndroidAndHandleAllWebDataUriTrue()358   public void createTitleChooser_openUrl_packageIsAndroidAndHandleAllWebDataUriTrue() {
359     TitleChooser titleChooser =
360         ActionsSuggestionsHelper.createTitleChooser(ConversationAction.TYPE_OPEN_URL);
361     LabeledIntent labeledIntent = createWebLabeledIntent();
362 
363     assertThat(
364             titleChooser
365                 .chooseTitle(
366                     labeledIntent, createResolveInfo("android", /* handleAllWebDataURI= */ true))
367                 .toString())
368         .isEqualTo("titleWithEntity");
369   }
370 
createWebLabeledIntent()371   private LabeledIntent createWebLabeledIntent() {
372     Intent webIntent = new Intent(Intent.ACTION_VIEW);
373     webIntent.setData(Uri.parse("http://www.android.com"));
374     return new LabeledIntent(
375         "titleWithoutEntity",
376         "titleWithEntity",
377         "description",
378         "descriptionWithAppName",
379         webIntent,
380         /* requestCode= */ 0);
381   }
382 
createZonedDateTimeFromMsUtc(long msUtc)383   private static ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
384     return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneId.of("UTC"));
385   }
386 
assertNativeMessage( ActionsSuggestionsModel.ConversationMessage nativeMessage, CharSequence text, int userId, long referenceTimeInMsUtc)387   private static void assertNativeMessage(
388       ActionsSuggestionsModel.ConversationMessage nativeMessage,
389       CharSequence text,
390       int userId,
391       long referenceTimeInMsUtc) {
392     assertThat(nativeMessage.getText()).isEqualTo(text.toString());
393     assertThat(nativeMessage.getUserId()).isEqualTo(userId);
394     assertThat(nativeMessage.getDetectedTextLanguageTags()).isEqualTo(LOCALE_TAG);
395     assertThat(nativeMessage.getReferenceTimeMsUtc()).isEqualTo(referenceTimeInMsUtc);
396   }
397 
createResolveInfo(String packageName, boolean handleAllWebDataURI)398   private static ResolveInfo createResolveInfo(String packageName, boolean handleAllWebDataURI) {
399     ResolveInfo resolveInfo = new ResolveInfo();
400     resolveInfo.activityInfo = new ActivityInfo();
401     resolveInfo.activityInfo.packageName = packageName;
402     resolveInfo.handleAllWebDataURI = handleAllWebDataURI;
403     return resolveInfo;
404   }
405 }
406