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