• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.Intent
22 import android.content.pm.ShortcutInfo
23 import android.os.UserHandle
24 import android.platform.test.annotations.EnableFlags
25 import android.platform.test.flag.junit.SetFlagsRule
26 import android.service.chooser.ChooserTarget
27 import androidx.test.filters.SmallTest
28 import androidx.test.platform.app.InstrumentationRegistry
29 import com.android.intentresolver.Flags.FLAG_REBUILD_ADAPTERS_ON_TARGET_PINNING
30 import com.android.intentresolver.chooser.DisplayResolveInfo
31 import com.android.intentresolver.chooser.TargetInfo
32 import com.google.common.truth.Correspondence
33 import com.google.common.truth.Truth.assertThat
34 import com.google.common.truth.Truth.assertWithMessage
35 import org.junit.Rule
36 import org.junit.Test
37 import org.mockito.kotlin.doReturn
38 import org.mockito.kotlin.mock
39 
40 private const val PACKAGE_A = "package.a"
41 private const val PACKAGE_B = "package.b"
42 private const val CLASS_NAME = "./MainActivity"
43 
44 private val PERSONAL_USER_HANDLE: UserHandle =
45     InstrumentationRegistry.getInstrumentation().targetContext.user
46 
47 @SmallTest
48 class ShortcutSelectionLogicTest {
49     @get:Rule val flagRule = SetFlagsRule()
50 
51     private val packageTargets =
52         HashMap<String, Array<ChooserTarget>>().apply {
53             arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
54                 // shortcuts in reverse priority order
55                 val targets =
56                     Array(3) { i ->
57                         createChooserTarget(
58                             "Shortcut $i",
59                             (i + 1).toFloat() / 10f,
60                             ComponentName(pkg, CLASS_NAME),
61                             pkg.shortcutId(i),
62                         )
63                     }
64                 this[pkg] = targets
65             }
66         }
67     private val targetInfoChooserTargetCorrespondence =
68         Correspondence.from<TargetInfo, ChooserTarget>(
69             { actual, expected ->
70                 actual.chooserTargetComponentName == expected.componentName &&
71                     actual.displayLabel == expected.title
72             },
73             "",
74         )
75 
76     private val baseDisplayInfo =
77         DisplayResolveInfo.newDisplayResolveInfo(
78             Intent(),
79             ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
80             "label",
81             "extended info",
82             Intent(),
83         )
84 
85     private val otherBaseDisplayInfo =
86         DisplayResolveInfo.newDisplayResolveInfo(
87             Intent(),
88             ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE),
89             "label 2",
90             "extended info 2",
91             Intent(),
92         )
93 
94     private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
95         this[pkg]?.get(idx) ?: error("missing package $pkg")
96 
97     @Test
98     fun testAddShortcuts_no_limits() {
99         val serviceResults = ArrayList<TargetInfo>()
100         val sc1 = packageTargets[PACKAGE_A, 0]
101         val sc2 = packageTargets[PACKAGE_A, 1]
102         val testSubject =
103             ShortcutSelectionLogic(
104                 /* maxShortcutTargetsPerApp = */ 1,
105                 /* applySharingAppLimits = */ false,
106             )
107 
108         val isUpdated =
109             testSubject.addServiceResults(
110                 /* origTarget = */ baseDisplayInfo,
111                 /* origTargetScore = */ 0.1f,
112                 /* targets = */ listOf(sc1, sc2),
113                 /* isShortcutResult = */ true,
114                 /* directShareToShortcutInfos = */ emptyMap(),
115                 /* directShareToAppTargets = */ emptyMap(),
116                 /* userContext = */ mock(),
117                 /* targetIntent = */ mock(),
118                 /* refererFillInIntent = */ mock(),
119                 /* maxRankedTargets = */ 4,
120                 /* serviceTargets = */ serviceResults,
121             )
122 
123         assertWithMessage("Updates are expected").that(isUpdated).isTrue()
124         assertWithMessage("Two shortcuts are expected as we do not apply per-app shortcut limit")
125             .that(serviceResults)
126             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
127             .containsExactly(sc2, sc1)
128             .inOrder()
129     }
130 
131     @Test
132     fun testAddShortcuts_same_package_with_per_package_limit() {
133         val serviceResults = ArrayList<TargetInfo>()
134         val sc1 = packageTargets[PACKAGE_A, 0]
135         val sc2 = packageTargets[PACKAGE_A, 1]
136         val testSubject =
137             ShortcutSelectionLogic(
138                 /* maxShortcutTargetsPerApp = */ 1,
139                 /* applySharingAppLimits = */ true,
140             )
141 
142         val isUpdated =
143             testSubject.addServiceResults(
144                 /* origTarget = */ baseDisplayInfo,
145                 /* origTargetScore = */ 0.1f,
146                 /* targets = */ listOf(sc1, sc2),
147                 /* isShortcutResult = */ true,
148                 /* directShareToShortcutInfos = */ emptyMap(),
149                 /* directShareToAppTargets = */ emptyMap(),
150                 /* userContext = */ mock(),
151                 /* targetIntent = */ mock(),
152                 /* refererFillInIntent = */ mock(),
153                 /* maxRankedTargets = */ 4,
154                 /* serviceTargets = */ serviceResults,
155             )
156 
157         assertWithMessage("Updates are expected").that(isUpdated).isTrue()
158         assertWithMessage("One shortcut is expected as we apply per-app shortcut limit")
159             .that(serviceResults)
160             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
161             .containsExactly(sc2)
162             .inOrder()
163     }
164 
165     @Test
166     fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() {
167         val serviceResults = ArrayList<TargetInfo>()
168         val sc1 = packageTargets[PACKAGE_A, 0]
169         val sc2 = packageTargets[PACKAGE_A, 1]
170         val testSubject =
171             ShortcutSelectionLogic(
172                 /* maxShortcutTargetsPerApp = */ 1,
173                 /* applySharingAppLimits = */ false,
174             )
175 
176         val isUpdated =
177             testSubject.addServiceResults(
178                 /* origTarget = */ baseDisplayInfo,
179                 /* origTargetScore = */ 0.1f,
180                 /* targets = */ listOf(sc1, sc2),
181                 /* isShortcutResult = */ true,
182                 /* directShareToShortcutInfos = */ emptyMap(),
183                 /* directShareToAppTargets = */ emptyMap(),
184                 /* userContext = */ mock(),
185                 /* targetIntent = */ mock(),
186                 /* refererFillInIntent = */ mock(),
187                 /* maxRankedTargets = */ 1,
188                 /* serviceTargets = */ serviceResults,
189             )
190 
191         assertWithMessage("Updates are expected").that(isUpdated).isTrue()
192         assertWithMessage("One shortcut is expected as we apply overall shortcut limit")
193             .that(serviceResults)
194             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
195             .containsExactly(sc2)
196             .inOrder()
197     }
198 
199     @Test
200     fun testAddShortcuts_different_packages_with_per_package_limit() {
201         val serviceResults = ArrayList<TargetInfo>()
202         val pkgAsc1 = packageTargets[PACKAGE_A, 0]
203         val pkgAsc2 = packageTargets[PACKAGE_A, 1]
204         val pkgBsc1 = packageTargets[PACKAGE_B, 0]
205         val pkgBsc2 = packageTargets[PACKAGE_B, 1]
206         val testSubject =
207             ShortcutSelectionLogic(
208                 /* maxShortcutTargetsPerApp = */ 1,
209                 /* applySharingAppLimits = */ true,
210             )
211 
212         testSubject.addServiceResults(
213             /* origTarget = */ baseDisplayInfo,
214             /* origTargetScore = */ 0.1f,
215             /* targets = */ listOf(pkgAsc1, pkgAsc2),
216             /* isShortcutResult = */ true,
217             /* directShareToShortcutInfos = */ emptyMap(),
218             /* directShareToAppTargets = */ emptyMap(),
219             /* userContext = */ mock(),
220             /* targetIntent = */ mock(),
221             /* refererFillInIntent = */ mock(),
222             /* maxRankedTargets = */ 4,
223             /* serviceTargets = */ serviceResults,
224         )
225         testSubject.addServiceResults(
226             /* origTarget = */ otherBaseDisplayInfo,
227             /* origTargetScore = */ 0.2f,
228             /* targets = */ listOf(pkgBsc1, pkgBsc2),
229             /* isShortcutResult = */ true,
230             /* directShareToShortcutInfos = */ emptyMap(),
231             /* directShareToAppTargets = */ emptyMap(),
232             /* userContext = */ mock(),
233             /* targetIntent = */ mock(),
234             /* refererFillInIntent = */ mock(),
235             /* maxRankedTargets = */ 4,
236             /* serviceTargets = */ serviceResults,
237         )
238 
239         assertWithMessage("Two shortcuts are expected as we apply per-app shortcut limit")
240             .that(serviceResults)
241             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
242             .containsExactly(pkgBsc2, pkgAsc2)
243             .inOrder()
244     }
245 
246     @Test
247     fun testAddShortcuts_pinned_shortcut() {
248         val serviceResults = ArrayList<TargetInfo>()
249         val sc1 = packageTargets[PACKAGE_A, 0]
250         val sc2 = packageTargets[PACKAGE_A, 1]
251         val testSubject =
252             ShortcutSelectionLogic(
253                 /* maxShortcutTargetsPerApp = */ 1,
254                 /* applySharingAppLimits = */ false,
255             )
256 
257         val isUpdated =
258             testSubject.addServiceResults(
259                 /* origTarget = */ baseDisplayInfo,
260                 /* origTargetScore = */ 0.1f,
261                 /* targets = */ listOf(sc1, sc2),
262                 /* isShortcutResult = */ true,
263                 /* directShareToShortcutInfos = */ mapOf(
264                     sc1 to
265                         createShortcutInfo(PACKAGE_A.shortcutId(1), sc1.componentName, 1).apply {
266                             addFlags(ShortcutInfo.FLAG_PINNED)
267                         }
268                 ),
269                 /* directShareToAppTargets = */ emptyMap(),
270                 /* userContext = */ mock(),
271                 /* targetIntent = */ mock(),
272                 /* refererFillInIntent = */ mock(),
273                 /* maxRankedTargets = */ 4,
274                 /* serviceTargets = */ serviceResults,
275             )
276 
277         assertWithMessage("Updates are expected").that(isUpdated).isTrue()
278         assertWithMessage("Two shortcuts are expected as we do not apply per-app shortcut limit")
279             .that(serviceResults)
280             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
281             .containsExactly(sc1, sc2)
282             .inOrder()
283     }
284 
285     @Test
286     fun test_available_caller_shortcuts_count_is_limited() {
287         val serviceResults = ArrayList<TargetInfo>()
288         val sc1 = packageTargets[PACKAGE_A, 0]
289         val sc2 = packageTargets[PACKAGE_A, 1]
290         val sc3 = packageTargets[PACKAGE_A, 2]
291         val testSubject =
292             ShortcutSelectionLogic(
293                 /* maxShortcutTargetsPerApp = */ 1,
294                 /* applySharingAppLimits = */ true,
295             )
296         val context = mock<Context> { on { packageManager } doReturn (mock()) }
297 
298         testSubject.addServiceResults(
299             /* origTarget = */ baseDisplayInfo,
300             /* origTargetScore = */ 0f,
301             /* targets = */ listOf(sc1, sc2, sc3),
302             /* isShortcutResult = */ false,
303             /* directShareToShortcutInfos = */ emptyMap(),
304             /* directShareToAppTargets = */ emptyMap(),
305             /* userContext = */ context,
306             /* targetIntent = */ mock(),
307             /* refererFillInIntent = */ mock(),
308             /* maxRankedTargets = */ 4,
309             /* serviceTargets = */ serviceResults,
310         )
311 
312         assertWithMessage("At most two caller-provided shortcuts are allowed")
313             .that(serviceResults)
314             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
315             .containsExactly(sc3, sc2)
316             .inOrder()
317     }
318 
319     @Test
320     @EnableFlags(FLAG_REBUILD_ADAPTERS_ON_TARGET_PINNING)
321     fun addServiceResults_sameShortcutWithDifferentPinnedStatus_shortcutUpdated() {
322         val serviceResults = ArrayList<TargetInfo>()
323         val sc1 =
324             createChooserTarget(
325                 title = "Shortcut",
326                 score = 1f,
327                 ComponentName(PACKAGE_A, CLASS_NAME),
328                 PACKAGE_A.shortcutId(0),
329             )
330         val sc2 =
331             createChooserTarget(
332                 title = "Shortcut",
333                 score = 1f,
334                 ComponentName(PACKAGE_A, CLASS_NAME),
335                 PACKAGE_A.shortcutId(0),
336             )
337         val testSubject =
338             ShortcutSelectionLogic(
339                 /* maxShortcutTargetsPerApp = */ 1,
340                 /* applySharingAppLimits = */ false,
341             )
342 
343         testSubject.addServiceResults(
344             /* origTarget = */ baseDisplayInfo,
345             /* origTargetScore = */ 0.1f,
346             /* targets = */ listOf(sc1),
347             /* isShortcutResult = */ true,
348             /* directShareToShortcutInfos = */ mapOf(
349                 sc1 to createShortcutInfo(PACKAGE_A.shortcutId(1), sc1.componentName, 1)
350             ),
351             /* directShareToAppTargets = */ emptyMap(),
352             /* userContext = */ mock(),
353             /* targetIntent = */ mock(),
354             /* refererFillInIntent = */ mock(),
355             /* maxRankedTargets = */ 4,
356             /* serviceTargets = */ serviceResults,
357         )
358         val isUpdated =
359             testSubject.addServiceResults(
360                 /* origTarget = */ baseDisplayInfo,
361                 /* origTargetScore = */ 0.1f,
362                 /* targets = */ listOf(sc1),
363                 /* isShortcutResult = */ true,
364                 /* directShareToShortcutInfos = */ mapOf(
365                     sc1 to
366                         createShortcutInfo(PACKAGE_A.shortcutId(1), sc1.componentName, 1).apply {
367                             addFlags(ShortcutInfo.FLAG_PINNED)
368                         }
369                 ),
370                 /* directShareToAppTargets = */ emptyMap(),
371                 /* userContext = */ mock(),
372                 /* targetIntent = */ mock(),
373                 /* refererFillInIntent = */ mock(),
374                 /* maxRankedTargets = */ 4,
375                 /* serviceTargets = */ serviceResults,
376             )
377 
378         assertWithMessage("Updates are expected").that(isUpdated).isTrue()
379         assertWithMessage("Updated shortcut is expected")
380             .that(serviceResults)
381             .comparingElementsUsing(targetInfoChooserTargetCorrespondence)
382             .containsExactly(sc2)
383             .inOrder()
384         assertThat(serviceResults[0].isPinned).isTrue()
385     }
386 
387     private fun String.shortcutId(id: Int) = "$this.$id"
388 }
389