1 /* 2 * Copyright (C) 2022 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.intentresolver.chooser 18 19 import android.app.prediction.AppTarget 20 import android.app.prediction.AppTargetId 21 import android.content.ComponentName 22 import android.content.Intent 23 import android.content.pm.ActivityInfo 24 import android.content.pm.ResolveInfo 25 import android.graphics.drawable.AnimatedVectorDrawable 26 import android.os.UserHandle 27 import androidx.test.annotation.UiThreadTest 28 import androidx.test.platform.app.InstrumentationRegistry 29 import com.android.intentresolver.ResolverDataProvider 30 import com.android.intentresolver.ResolverDataProvider.createResolveInfo 31 import com.android.intentresolver.createChooserTarget 32 import com.android.intentresolver.createShortcutInfo 33 import com.google.common.truth.Truth.assertThat 34 import org.junit.Before 35 import org.junit.Test 36 import org.mockito.kotlin.any 37 import org.mockito.kotlin.doReturn 38 import org.mockito.kotlin.mock 39 import org.mockito.kotlin.never 40 import org.mockito.kotlin.spy 41 import org.mockito.kotlin.times 42 import org.mockito.kotlin.verify 43 44 class TargetInfoTest { 45 private val PERSONAL_USER_HANDLE: UserHandle = 46 InstrumentationRegistry.getInstrumentation().getTargetContext().getUser() 47 48 private val context = InstrumentationRegistry.getInstrumentation().getContext() 49 50 @Before setupnull51 fun setup() { 52 // SelectableTargetInfo reads DeviceConfig and needs a permission for that. 53 InstrumentationRegistry.getInstrumentation() 54 .uiAutomation 55 .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") 56 } 57 58 @Test testNewEmptyTargetInfonull59 fun testNewEmptyTargetInfo() { 60 val info = NotSelectableTargetInfo.newEmptyTargetInfo() 61 assertThat(info.isEmptyTargetInfo).isTrue() 62 assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model. 63 assertThat(info.hasDisplayIcon()).isFalse() 64 assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull() 65 } 66 67 @UiThreadTest // AnimatedVectorDrawable needs to start from a thread with a Looper. 68 @Test testNewPlaceholderTargetInfonull69 fun testNewPlaceholderTargetInfo() { 70 val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context) 71 assertThat(info.isPlaceHolderTargetInfo).isTrue() 72 assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model. 73 assertThat(info.hasDisplayIcon()).isTrue() 74 assertThat(info.displayIconHolder.displayIcon) 75 .isInstanceOf(AnimatedVectorDrawable::class.java) 76 // TODO: assert that the animation is pre-started/running (IIUC this requires synchronizing 77 // with some "render thread" per the `AnimatedVectorDrawable` docs). I believe this is 78 // possible using `AnimatorTestRule` but I couldn't find any sample usage in Kotlin nor get 79 // it working myself. 80 } 81 82 @Test testNewSelectableTargetInfonull83 fun testNewSelectableTargetInfo() { 84 val resolvedIntent = Intent() 85 val baseDisplayInfo = 86 DisplayResolveInfo.newDisplayResolveInfo( 87 resolvedIntent, 88 createResolveInfo(1, 0, PERSONAL_USER_HANDLE), 89 "label", 90 "extended info", 91 resolvedIntent 92 ) 93 val chooserTarget = 94 createChooserTarget( 95 "title", 96 0.3f, 97 ResolverDataProvider.createComponentName(2), 98 "test_shortcut_id" 99 ) 100 val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) 101 val appTarget = 102 AppTarget( 103 AppTargetId("id"), 104 chooserTarget.componentName.packageName, 105 chooserTarget.componentName.className, 106 UserHandle.CURRENT 107 ) 108 109 val targetInfo = 110 SelectableTargetInfo.newSelectableTargetInfo( 111 baseDisplayInfo, 112 mock(), 113 resolvedIntent, 114 chooserTarget, 115 0.1f, 116 shortcutInfo, 117 appTarget, 118 mock(), 119 ) 120 assertThat(targetInfo.isSelectableTargetInfo).isTrue() 121 assertThat(targetInfo.isChooserTargetInfo).isTrue() // From legacy inheritance model. 122 assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(baseDisplayInfo) 123 assertThat(targetInfo.chooserTargetComponentName).isEqualTo(chooserTarget.componentName) 124 assertThat(targetInfo.directShareShortcutId).isEqualTo(shortcutInfo.id) 125 assertThat(targetInfo.directShareShortcutInfo).isSameInstanceAs(shortcutInfo) 126 assertThat(targetInfo.directShareAppTarget).isSameInstanceAs(appTarget) 127 assertThat(targetInfo.resolvedIntent).isSameInstanceAs(resolvedIntent) 128 // TODO: make more meaningful assertions about the behavior of a selectable target. 129 } 130 131 @Test test_SelectableTargetInfo_componentName_no_source_infonull132 fun test_SelectableTargetInfo_componentName_no_source_info() { 133 val chooserTarget = 134 createChooserTarget( 135 "title", 136 0.3f, 137 ResolverDataProvider.createComponentName(1), 138 "test_shortcut_id" 139 ) 140 val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(2), 3) 141 val appTarget = 142 AppTarget( 143 AppTargetId("id"), 144 chooserTarget.componentName.packageName, 145 chooserTarget.componentName.className, 146 UserHandle.CURRENT 147 ) 148 val pkgName = "org.package" 149 val className = "MainActivity" 150 val backupResolveInfo = 151 ResolveInfo().apply { 152 activityInfo = 153 ActivityInfo().apply { 154 packageName = pkgName 155 name = className 156 } 157 } 158 159 val targetInfo = 160 SelectableTargetInfo.newSelectableTargetInfo( 161 null, 162 backupResolveInfo, 163 mock(), 164 chooserTarget, 165 0.1f, 166 shortcutInfo, 167 appTarget, 168 mock(), 169 ) 170 assertThat(targetInfo.resolvedComponentName).isEqualTo(ComponentName(pkgName, className)) 171 } 172 173 @Test testSelectableTargetInfo_noSourceIntentMatchingProposedRefinementnull174 fun testSelectableTargetInfo_noSourceIntentMatchingProposedRefinement() { 175 val resolvedIntent = Intent("DONT_REFINE_ME") 176 resolvedIntent.putExtra("resolvedIntent", true) 177 178 val baseDisplayInfo = 179 DisplayResolveInfo.newDisplayResolveInfo( 180 resolvedIntent, 181 createResolveInfo(1, 0), 182 "label", 183 "extended info", 184 resolvedIntent 185 ) 186 val chooserTarget = 187 createChooserTarget( 188 "title", 189 0.3f, 190 ResolverDataProvider.createComponentName(2), 191 "test_shortcut_id" 192 ) 193 val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) 194 val appTarget = 195 AppTarget( 196 AppTargetId("id"), 197 chooserTarget.componentName.packageName, 198 chooserTarget.componentName.className, 199 UserHandle.CURRENT 200 ) 201 202 val targetInfo = 203 SelectableTargetInfo.newSelectableTargetInfo( 204 baseDisplayInfo, 205 mock(), 206 resolvedIntent, 207 chooserTarget, 208 0.1f, 209 shortcutInfo, 210 appTarget, 211 mock(), 212 ) 213 214 val refinement = Intent("PROPOSED_REFINEMENT") 215 assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() 216 } 217 218 @Test testNewDisplayResolveInfonull219 fun testNewDisplayResolveInfo() { 220 val intent = Intent(Intent.ACTION_SEND) 221 intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") 222 intent.setType("text/plain") 223 224 val resolveInfo = createResolveInfo(3, 0, PERSONAL_USER_HANDLE) 225 226 val targetInfo = 227 DisplayResolveInfo.newDisplayResolveInfo( 228 intent, 229 resolveInfo, 230 "label", 231 "extended info", 232 intent 233 ) 234 assertThat(targetInfo.isDisplayResolveInfo).isTrue() 235 assertThat(targetInfo.isMultiDisplayResolveInfo).isFalse() 236 assertThat(targetInfo.isChooserTargetInfo).isFalse() 237 } 238 239 @Test test_DisplayResolveInfo_refinementToAlternateSourceIntentnull240 fun test_DisplayResolveInfo_refinementToAlternateSourceIntent() { 241 val originalIntent = Intent("DONT_REFINE_ME") 242 originalIntent.putExtra("originalIntent", true) 243 val mismatchedAlternate = Intent("DOESNT_MATCH") 244 mismatchedAlternate.putExtra("mismatchedAlternate", true) 245 val targetAlternate = Intent("REFINE_ME") 246 targetAlternate.putExtra("targetAlternate", true) 247 val extraMatch = Intent("REFINE_ME") 248 extraMatch.putExtra("extraMatch", true) 249 250 val originalInfo = 251 DisplayResolveInfo.newDisplayResolveInfo( 252 originalIntent, 253 createResolveInfo(3, 0), 254 "label", 255 "extended info", 256 originalIntent 257 ) 258 originalInfo.addAlternateSourceIntent(mismatchedAlternate) 259 originalInfo.addAlternateSourceIntent(targetAlternate) 260 originalInfo.addAlternateSourceIntent(extraMatch) 261 262 val refinement = Intent("REFINE_ME") // First match is `targetAlternate` 263 refinement.putExtra("refinement", true) 264 265 val refinedResult = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinement)) 266 // Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`. 267 assertThat(refinedResult.resolvedIntent?.getBooleanExtra("refinement", false)).isTrue() 268 assertThat(refinedResult.resolvedIntent?.getBooleanExtra("targetAlternate", false)).isTrue() 269 // None of the other source intents got merged in (not even the later one that matched): 270 assertThat(refinedResult.resolvedIntent?.getBooleanExtra("originalIntent", false)).isFalse() 271 assertThat(refinedResult.resolvedIntent?.getBooleanExtra("mismatchedAlternate", false)) 272 .isFalse() 273 assertThat(refinedResult.resolvedIntent?.getBooleanExtra("extraMatch", false)).isFalse() 274 } 275 276 @Test testDisplayResolveInfo_noSourceIntentMatchingProposedRefinementnull277 fun testDisplayResolveInfo_noSourceIntentMatchingProposedRefinement() { 278 val originalIntent = Intent("DONT_REFINE_ME") 279 originalIntent.putExtra("originalIntent", true) 280 val mismatchedAlternate = Intent("DOESNT_MATCH") 281 mismatchedAlternate.putExtra("mismatchedAlternate", true) 282 283 val originalInfo = 284 DisplayResolveInfo.newDisplayResolveInfo( 285 originalIntent, 286 createResolveInfo(3, 0), 287 "label", 288 "extended info", 289 originalIntent 290 ) 291 originalInfo.addAlternateSourceIntent(mismatchedAlternate) 292 293 val refinement = Intent("PROPOSED_REFINEMENT") 294 assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() 295 } 296 297 @Test testNewMultiDisplayResolveInfonull298 fun testNewMultiDisplayResolveInfo() { 299 val intent = Intent(Intent.ACTION_SEND) 300 intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") 301 intent.setType("text/plain") 302 303 val packageName = "org.pkg.app" 304 val componentA = ComponentName(packageName, "org.pkg.app.ActivityA") 305 val componentB = ComponentName(packageName, "org.pkg.app.ActivityB") 306 val resolveInfoA = createResolveInfo(componentA, 0, PERSONAL_USER_HANDLE) 307 val resolveInfoB = createResolveInfo(componentB, 0, PERSONAL_USER_HANDLE) 308 val firstTargetInfo = 309 DisplayResolveInfo.newDisplayResolveInfo( 310 intent, 311 resolveInfoA, 312 "label 1", 313 "extended info 1", 314 intent 315 ) 316 val secondTargetInfo = 317 DisplayResolveInfo.newDisplayResolveInfo( 318 intent, 319 resolveInfoB, 320 "label 2", 321 "extended info 2", 322 intent 323 ) 324 325 val multiTargetInfo = 326 MultiDisplayResolveInfo.newMultiDisplayResolveInfo( 327 listOf(firstTargetInfo, secondTargetInfo) 328 ) 329 330 assertThat(multiTargetInfo.isMultiDisplayResolveInfo).isTrue() 331 assertThat(multiTargetInfo.isDisplayResolveInfo).isTrue() // From legacy inheritance. 332 assertThat(multiTargetInfo.isChooserTargetInfo).isFalse() 333 334 assertThat(multiTargetInfo.extendedInfo).isNull() 335 336 assertThat(multiTargetInfo.allDisplayTargets) 337 .containsExactly(firstTargetInfo, secondTargetInfo) 338 339 assertThat(multiTargetInfo.hasSelected()).isFalse() 340 assertThat(multiTargetInfo.selectedTarget).isNull() 341 342 multiTargetInfo.setSelected(1) 343 344 assertThat(multiTargetInfo.hasSelected()).isTrue() 345 assertThat(multiTargetInfo.selectedTarget).isEqualTo(secondTargetInfo) 346 assertThat(multiTargetInfo.resolvedComponentName).isEqualTo(componentB) 347 348 val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent) 349 assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java) 350 assertThat((refined as MultiDisplayResolveInfo).hasSelected()) 351 .isEqualTo(multiTargetInfo.hasSelected()) 352 353 // TODO: consider exercising activity-start behavior. 354 // TODO: consider exercising DisplayResolveInfo base class behavior. 355 } 356 357 @Test testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTargetnull358 fun testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTarget() { 359 val sendImage = Intent("SEND").apply { type = "image/png" } 360 val sendUri = Intent("SEND").apply { type = "text/uri" } 361 362 val resolveInfo = createResolveInfo(1, 0) 363 364 val imageOnlyTarget = 365 DisplayResolveInfo.newDisplayResolveInfo( 366 sendImage, 367 resolveInfo, 368 "Send Image", 369 "Sends only images", 370 sendImage 371 ) 372 373 val textOnlyTarget = 374 DisplayResolveInfo.newDisplayResolveInfo( 375 sendUri, 376 resolveInfo, 377 "Send Text", 378 "Sends only text", 379 sendUri 380 ) 381 382 val imageOrTextTarget = 383 DisplayResolveInfo.newDisplayResolveInfo( 384 sendImage, 385 resolveInfo, 386 "Send Image or Text", 387 "Sends images or text", 388 sendImage 389 ) 390 .apply { addAlternateSourceIntent(sendUri) } 391 392 val multiTargetInfo = 393 MultiDisplayResolveInfo.newMultiDisplayResolveInfo( 394 listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget) 395 ) 396 397 multiTargetInfo.setSelected(0) 398 assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget) 399 assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOnlyTarget.allSourceIntents) 400 401 multiTargetInfo.setSelected(1) 402 assertThat(multiTargetInfo.selectedTarget).isEqualTo(textOnlyTarget) 403 assertThat(multiTargetInfo.allSourceIntents).isEqualTo(textOnlyTarget.allSourceIntents) 404 405 multiTargetInfo.setSelected(2) 406 assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOrTextTarget) 407 assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOrTextTarget.allSourceIntents) 408 } 409 410 @Test testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTargetnull411 fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() { 412 val refined = Intent("SEND") 413 val sendImage = Intent("SEND") 414 val targetOne = 415 spy( 416 DisplayResolveInfo.newDisplayResolveInfo( 417 sendImage, 418 createResolveInfo(1, 0), 419 "Target One", 420 "Target One", 421 sendImage 422 ) 423 ) 424 val targetTwo = 425 mock<DisplayResolveInfo> { on { tryToCloneWithAppliedRefinement(any()) } doReturn mock } 426 427 val multiTargetInfo = 428 MultiDisplayResolveInfo.newMultiDisplayResolveInfo(listOf(targetOne, targetTwo)) 429 430 multiTargetInfo.setSelected(1) 431 assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo) 432 433 multiTargetInfo.tryToCloneWithAppliedRefinement(refined) 434 verify(targetTwo, times(1)).tryToCloneWithAppliedRefinement(refined) 435 verify(targetOne, never()).tryToCloneWithAppliedRefinement(any()) 436 } 437 } 438