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.service.chooser.ChooserTarget
25 import androidx.test.filters.SmallTest
26 import androidx.test.platform.app.InstrumentationRegistry
27 import com.android.intentresolver.chooser.DisplayResolveInfo
28 import com.android.intentresolver.chooser.TargetInfo
29 import org.junit.Assert.assertEquals
30 import org.junit.Assert.assertTrue
31 import org.junit.Test
32 import org.mockito.kotlin.doReturn
33 import org.mockito.kotlin.mock
34 
35 private const val PACKAGE_A = "package.a"
36 private const val PACKAGE_B = "package.b"
37 private const val CLASS_NAME = "./MainActivity"
38 
39 @SmallTest
40 class ShortcutSelectionLogicTest {
41     private val PERSONAL_USER_HANDLE: UserHandle =
42         InstrumentationRegistry.getInstrumentation().getTargetContext().getUser()
43 
44     private val packageTargets =
45         HashMap<String, Array<ChooserTarget>>().apply {
46             arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
47                 // shortcuts in reverse priority order
48                 val targets =
49                     Array(3) { i ->
50                         createChooserTarget(
51                             "Shortcut $i",
52                             (i + 1).toFloat() / 10f,
53                             ComponentName(pkg, CLASS_NAME),
54                             pkg.shortcutId(i),
55                         )
56                     }
57                 this[pkg] = targets
58             }
59         }
60 
61     private val baseDisplayInfo =
62         DisplayResolveInfo.newDisplayResolveInfo(
63             Intent(),
64             ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
65             "label",
66             "extended info",
67             Intent()
68         )
69 
70     private val otherBaseDisplayInfo =
71         DisplayResolveInfo.newDisplayResolveInfo(
72             Intent(),
73             ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE),
74             "label 2",
75             "extended info 2",
76             Intent()
77         )
78 
79     private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
80         this[pkg]?.get(idx) ?: error("missing package $pkg")
81 
82     @Test
83     fun testAddShortcuts_no_limits() {
84         val serviceResults = ArrayList<TargetInfo>()
85         val sc1 = packageTargets[PACKAGE_A, 0]
86         val sc2 = packageTargets[PACKAGE_A, 1]
87         val testSubject =
88             ShortcutSelectionLogic(
89                 /* maxShortcutTargetsPerApp = */ 1,
90                 /* applySharingAppLimits = */ false
91             )
92 
93         val isUpdated =
94             testSubject.addServiceResults(
95                 /* origTarget = */ baseDisplayInfo,
96                 /* origTargetScore = */ 0.1f,
97                 /* targets = */ listOf(sc1, sc2),
98                 /* isShortcutResult = */ true,
99                 /* directShareToShortcutInfos = */ emptyMap(),
100                 /* directShareToAppTargets = */ emptyMap(),
101                 /* userContext = */ mock(),
102                 /* targetIntent = */ mock(),
103                 /* refererFillInIntent = */ mock(),
104                 /* maxRankedTargets = */ 4,
105                 /* serviceTargets = */ serviceResults
106             )
107 
108         assertTrue("Updates are expected", isUpdated)
109         assertShortcutsInOrder(
110             listOf(sc2, sc1),
111             serviceResults,
112             "Two shortcuts are expected as we do not apply per-app shortcut limit"
113         )
114     }
115 
116     @Test
117     fun testAddShortcuts_same_package_with_per_package_limit() {
118         val serviceResults = ArrayList<TargetInfo>()
119         val sc1 = packageTargets[PACKAGE_A, 0]
120         val sc2 = packageTargets[PACKAGE_A, 1]
121         val testSubject =
122             ShortcutSelectionLogic(
123                 /* maxShortcutTargetsPerApp = */ 1,
124                 /* applySharingAppLimits = */ true
125             )
126 
127         val isUpdated =
128             testSubject.addServiceResults(
129                 /* origTarget = */ baseDisplayInfo,
130                 /* origTargetScore = */ 0.1f,
131                 /* targets = */ listOf(sc1, sc2),
132                 /* isShortcutResult = */ true,
133                 /* directShareToShortcutInfos = */ emptyMap(),
134                 /* directShareToAppTargets = */ emptyMap(),
135                 /* userContext = */ mock(),
136                 /* targetIntent = */ mock(),
137                 /* refererFillInIntent = */ mock(),
138                 /* maxRankedTargets = */ 4,
139                 /* serviceTargets = */ serviceResults
140             )
141 
142         assertTrue("Updates are expected", isUpdated)
143         assertShortcutsInOrder(
144             listOf(sc2),
145             serviceResults,
146             "One shortcut is expected as we apply per-app shortcut limit"
147         )
148     }
149 
150     @Test
151     fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() {
152         val serviceResults = ArrayList<TargetInfo>()
153         val sc1 = packageTargets[PACKAGE_A, 0]
154         val sc2 = packageTargets[PACKAGE_A, 1]
155         val testSubject =
156             ShortcutSelectionLogic(
157                 /* maxShortcutTargetsPerApp = */ 1,
158                 /* applySharingAppLimits = */ false
159             )
160 
161         val isUpdated =
162             testSubject.addServiceResults(
163                 /* origTarget = */ baseDisplayInfo,
164                 /* origTargetScore = */ 0.1f,
165                 /* targets = */ listOf(sc1, sc2),
166                 /* isShortcutResult = */ true,
167                 /* directShareToShortcutInfos = */ emptyMap(),
168                 /* directShareToAppTargets = */ emptyMap(),
169                 /* userContext = */ mock(),
170                 /* targetIntent = */ mock(),
171                 /* refererFillInIntent = */ mock(),
172                 /* maxRankedTargets = */ 1,
173                 /* serviceTargets = */ serviceResults
174             )
175 
176         assertTrue("Updates are expected", isUpdated)
177         assertShortcutsInOrder(
178             listOf(sc2),
179             serviceResults,
180             "One shortcut is expected as we apply overall shortcut limit"
181         )
182     }
183 
184     @Test
185     fun testAddShortcuts_different_packages_with_per_package_limit() {
186         val serviceResults = ArrayList<TargetInfo>()
187         val pkgAsc1 = packageTargets[PACKAGE_A, 0]
188         val pkgAsc2 = packageTargets[PACKAGE_A, 1]
189         val pkgBsc1 = packageTargets[PACKAGE_B, 0]
190         val pkgBsc2 = packageTargets[PACKAGE_B, 1]
191         val testSubject =
192             ShortcutSelectionLogic(
193                 /* maxShortcutTargetsPerApp = */ 1,
194                 /* applySharingAppLimits = */ true
195             )
196 
197         testSubject.addServiceResults(
198             /* origTarget = */ baseDisplayInfo,
199             /* origTargetScore = */ 0.1f,
200             /* targets = */ listOf(pkgAsc1, pkgAsc2),
201             /* isShortcutResult = */ true,
202             /* directShareToShortcutInfos = */ emptyMap(),
203             /* directShareToAppTargets = */ emptyMap(),
204             /* userContext = */ mock(),
205             /* targetIntent = */ mock(),
206             /* refererFillInIntent = */ mock(),
207             /* maxRankedTargets = */ 4,
208             /* serviceTargets = */ serviceResults
209         )
210         testSubject.addServiceResults(
211             /* origTarget = */ otherBaseDisplayInfo,
212             /* origTargetScore = */ 0.2f,
213             /* targets = */ listOf(pkgBsc1, pkgBsc2),
214             /* isShortcutResult = */ true,
215             /* directShareToShortcutInfos = */ emptyMap(),
216             /* directShareToAppTargets = */ emptyMap(),
217             /* userContext = */ mock(),
218             /* targetIntent = */ mock(),
219             /* refererFillInIntent = */ mock(),
220             /* maxRankedTargets = */ 4,
221             /* serviceTargets = */ serviceResults
222         )
223 
224         assertShortcutsInOrder(
225             listOf(pkgBsc2, pkgAsc2),
226             serviceResults,
227             "Two shortcuts are expected as we apply per-app shortcut limit"
228         )
229     }
230 
231     @Test
232     fun testAddShortcuts_pinned_shortcut() {
233         val serviceResults = ArrayList<TargetInfo>()
234         val sc1 = packageTargets[PACKAGE_A, 0]
235         val sc2 = packageTargets[PACKAGE_A, 1]
236         val testSubject =
237             ShortcutSelectionLogic(
238                 /* maxShortcutTargetsPerApp = */ 1,
239                 /* applySharingAppLimits = */ false
240             )
241 
242         val isUpdated =
243             testSubject.addServiceResults(
244                 /* origTarget = */ baseDisplayInfo,
245                 /* origTargetScore = */ 0.1f,
246                 /* targets = */ listOf(sc1, sc2),
247                 /* isShortcutResult = */ true,
248                 /* directShareToShortcutInfos = */ mapOf(
249                     sc1 to
250                         createShortcutInfo(PACKAGE_A.shortcutId(1), sc1.componentName, 1).apply {
251                             addFlags(ShortcutInfo.FLAG_PINNED)
252                         }
253                 ),
254                 /* directShareToAppTargets = */ emptyMap(),
255                 /* userContext = */ mock(),
256                 /* targetIntent = */ mock(),
257                 /* refererFillInIntent = */ mock(),
258                 /* maxRankedTargets = */ 4,
259                 /* serviceTargets = */ serviceResults
260             )
261 
262         assertTrue("Updates are expected", isUpdated)
263         assertShortcutsInOrder(
264             listOf(sc1, sc2),
265             serviceResults,
266             "Two shortcuts are expected as we do not apply per-app shortcut limit"
267         )
268     }
269 
270     @Test
271     fun test_available_caller_shortcuts_count_is_limited() {
272         val serviceResults = ArrayList<TargetInfo>()
273         val sc1 = packageTargets[PACKAGE_A, 0]
274         val sc2 = packageTargets[PACKAGE_A, 1]
275         val sc3 = packageTargets[PACKAGE_A, 2]
276         val testSubject =
277             ShortcutSelectionLogic(
278                 /* maxShortcutTargetsPerApp = */ 1,
279                 /* applySharingAppLimits = */ true
280             )
281         val context = mock<Context> { on { packageManager } doReturn (mock()) }
282 
283         testSubject.addServiceResults(
284             /* origTarget = */ baseDisplayInfo,
285             /* origTargetScore = */ 0f,
286             /* targets = */ listOf(sc1, sc2, sc3),
287             /* isShortcutResult = */ false,
288             /* directShareToShortcutInfos = */ emptyMap(),
289             /* directShareToAppTargets = */ emptyMap(),
290             /* userContext = */ context,
291             /* targetIntent = */ mock(),
292             /* refererFillInIntent = */ mock(),
293             /* maxRankedTargets = */ 4,
294             /* serviceTargets = */ serviceResults
295         )
296 
297         assertShortcutsInOrder(
298             listOf(sc3, sc2),
299             serviceResults,
300             "At most two caller-provided shortcuts are allowed"
301         )
302     }
303 
304     // TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases
305     // add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`.
306     private fun assertShortcutsInOrder(
307         expected: List<ChooserTarget>,
308         actual: List<TargetInfo>,
309         msg: String? = ""
310     ) {
311         assertEquals(msg, expected.size, actual.size)
312         for (i in expected.indices) {
313             assertEquals(
314                 "Unexpected item at position $i",
315                 expected[i].componentName,
316                 actual[i].chooserTargetComponentName
317             )
318             assertEquals(
319                 "Unexpected item at position $i",
320                 expected[i].title,
321                 actual[i].displayLabel
322             )
323         }
324     }
325 
326     private fun String.shortcutId(id: Int) = "$this.$id"
327 }
328