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