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