1 /* <lambda>null2 * Copyright (C) 2016 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 package com.android.intentresolver 17 18 import android.content.ClipData 19 import android.content.ClipDescription 20 import android.content.ComponentName 21 import android.content.Context 22 import android.content.Intent 23 import android.content.pm.PackageManager 24 import android.content.pm.ResolveInfo 25 import android.graphics.Color 26 import android.net.Uri 27 import android.os.UserHandle 28 import android.platform.test.flag.junit.CheckFlagsRule 29 import android.platform.test.flag.junit.DeviceFlagsValueProvider 30 import android.provider.DeviceConfig 31 import androidx.compose.ui.test.AndroidComposeUiTest 32 import androidx.compose.ui.test.AndroidComposeUiTestEnvironment 33 import androidx.compose.ui.test.ExperimentalTestApi 34 import androidx.compose.ui.test.hasScrollToIndexAction 35 import androidx.compose.ui.test.onNodeWithTag 36 import androidx.compose.ui.test.performClick 37 import androidx.compose.ui.test.performScrollToIndex 38 import androidx.test.core.app.ActivityScenario 39 import androidx.test.espresso.Espresso.onView 40 import androidx.test.espresso.action.ViewActions.click 41 import androidx.test.espresso.matcher.ViewMatchers 42 import androidx.test.espresso.matcher.ViewMatchers.withId 43 import androidx.test.espresso.matcher.ViewMatchers.withText 44 import androidx.test.platform.app.InstrumentationRegistry 45 import com.android.intentresolver.TestContentProvider.Companion.makeItemUri 46 import com.android.intentresolver.chooser.TargetInfo 47 import com.android.intentresolver.contentpreview.ImageLoader 48 import com.android.intentresolver.contentpreview.ImageLoaderModule 49 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.CursorResolver 50 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.FakePayloadToggleCursorResolver 51 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.FakePayloadToggleCursorResolver.Companion.DEFAULT_MIME_TYPE 52 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle 53 import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggleCursorResolver 54 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow 55 import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.FakeSelectionChangeCallback 56 import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback 57 import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallbackModule 58 import com.android.intentresolver.data.repository.FakeUserRepository 59 import com.android.intentresolver.data.repository.UserRepository 60 import com.android.intentresolver.data.repository.UserRepositoryModule 61 import com.android.intentresolver.inject.ApplicationUser 62 import com.android.intentresolver.inject.PackageManagerModule 63 import com.android.intentresolver.inject.ProfileParent 64 import com.android.intentresolver.platform.AppPredictionAvailable 65 import com.android.intentresolver.platform.AppPredictionModule 66 import com.android.intentresolver.platform.ImageEditor 67 import com.android.intentresolver.platform.ImageEditorModule 68 import com.android.intentresolver.shared.model.User 69 import com.android.intentresolver.tests.R 70 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags 71 import com.google.common.truth.Truth.assertThat 72 import dagger.hilt.android.qualifiers.ApplicationContext 73 import dagger.hilt.android.testing.BindValue 74 import dagger.hilt.android.testing.HiltAndroidRule 75 import dagger.hilt.android.testing.HiltAndroidTest 76 import dagger.hilt.android.testing.UninstallModules 77 import java.util.Optional 78 import java.util.concurrent.atomic.AtomicReference 79 import java.util.function.Function 80 import javax.inject.Inject 81 import kotlinx.coroutines.ExperimentalCoroutinesApi 82 import org.hamcrest.Matchers.allOf 83 import org.junit.Before 84 import org.junit.Rule 85 import org.junit.Test 86 import org.mockito.ArgumentMatchers.anyBoolean 87 import org.mockito.kotlin.any 88 import org.mockito.kotlin.doAnswer 89 import org.mockito.kotlin.stub 90 91 private const val TEST_TARGET_CATEGORY = "com.android.intentresolver.tests.TEST_RECEIVER_CATEGORY" 92 private const val PACKAGE = "com.android.intentresolver.tests" 93 private const val IMAGE_ACTIVITY = "com.android.intentresolver.tests.ImageReceiverActivity" 94 private const val VIDEO_ACTIVITY = "com.android.intentresolver.tests.VideoReceiverActivity" 95 private const val ALL_MEDIA_ACTIVITY = "com.android.intentresolver.tests.AllMediaReceiverActivity" 96 private const val IMAGE_ACTIVITY_LABEL = "ImageActivity" 97 private const val VIDEO_ACTIVITY_LABEL = "VideoActivity" 98 private const val ALL_MEDIA_ACTIVITY_LABEL = "AllMediaActivity" 99 100 /** 101 * Instrumentation tests for ChooserActivity. 102 * 103 * Legacy test suite migrated from framework CoreTests. 104 */ 105 @OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) 106 @HiltAndroidTest 107 @UninstallModules( 108 AppPredictionModule::class, 109 ImageEditorModule::class, 110 PackageManagerModule::class, 111 ImageLoaderModule::class, 112 UserRepositoryModule::class, 113 PayloadToggleCursorResolver.Binding::class, 114 SelectionChangeCallbackModule::class, 115 ) 116 class ChooserActivityShareouselTest() { 117 @get:Rule(order = 0) 118 val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() 119 120 @get:Rule(order = 1) val hiltAndroidRule: HiltAndroidRule = HiltAndroidRule(this) 121 122 @Inject @ApplicationContext lateinit var context: Context 123 124 @BindValue lateinit var packageManager: PackageManager 125 126 private val fakeUserRepo = FakeUserRepository(listOf(PERSONAL_USER)) 127 128 @BindValue val userRepository: UserRepository = fakeUserRepo 129 @AppPredictionAvailable @BindValue val appPredictionAvailable = false 130 131 private val fakeImageLoader = FakeImageLoader() 132 133 @BindValue val imageLoader: ImageLoader = fakeImageLoader 134 @BindValue 135 @ImageEditor 136 val imageEditor: Optional<ComponentName> = 137 Optional.ofNullable( 138 ComponentName.unflattenFromString( 139 "com.google.android.apps.messaging/.ui.conversationlist.ShareIntentActivity" 140 ) 141 ) 142 143 @BindValue @ApplicationUser val applicationUser = PERSONAL_USER_HANDLE 144 145 @BindValue @ProfileParent val profileParent = PERSONAL_USER_HANDLE 146 147 private val fakeCursorResolver = FakePayloadToggleCursorResolver() 148 @BindValue 149 @PayloadToggle 150 val additionalContentCursorResolver: CursorResolver<CursorRow?> = fakeCursorResolver 151 152 @BindValue val selectionChangeCallback: SelectionChangeCallback = FakeSelectionChangeCallback() 153 154 @Before 155 fun setUp() { 156 // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the 157 // permissions we require (which we'll read from the manifest at runtime). 158 InstrumentationRegistry.getInstrumentation().uiAutomation.adoptShellPermissionIdentity() 159 160 cleanOverrideData() 161 162 // Assign @Inject fields 163 hiltAndroidRule.inject() 164 165 // Populate @BindValue dependencies using injected values. These fields contribute 166 // values to the dependency graph at activity launch time. This allows replacing 167 // arbitrary bindings per-test case if needed. 168 packageManager = context.packageManager 169 with(ChooserActivityOverrideData.getInstance()) { 170 personalUserHandle = PERSONAL_USER_HANDLE 171 mockListController(resolverListController) 172 } 173 } 174 175 private fun setDeviceConfigProperty(propertyName: String, value: String) { 176 // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly 177 // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently 178 // configure in {@link #setup()}. 179 // TODO: is it really appropriate that this is always set with makeDefault=true? 180 val valueWasSet = 181 DeviceConfig.setProperty( 182 DeviceConfig.NAMESPACE_SYSTEMUI, 183 propertyName, 184 value, 185 true, /* makeDefault */ 186 ) 187 check(valueWasSet) { "Could not set $propertyName to $value" } 188 } 189 190 private fun cleanOverrideData() { 191 ChooserActivityOverrideData.getInstance().reset() 192 193 setDeviceConfigProperty( 194 SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, 195 true.toString(), 196 ) 197 } 198 199 @Test 200 fun test_shareInitiallySelectedItem_initiallySelectedItemShared() { 201 val launchedTargetInfo = AtomicReference<TargetInfo?>() 202 with(ChooserActivityOverrideData.getInstance()) { 203 onSafelyStartInternalCallback = 204 Function<TargetInfo, Boolean> { targetInfo -> 205 launchedTargetInfo.set(targetInfo) 206 true 207 } 208 } 209 val mimeTypes = emptyMap<Int, String>() 210 setBitmaps(mimeTypes) 211 fakeCursorResolver.setUris(count = 3, startPosition = 1, mimeTypes) 212 launchActivityWithComposeTestEnv(makeItemUri("1", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { 213 selectTarget(IMAGE_ACTIVITY_LABEL) 214 } 215 216 val launchedTarget = launchedTargetInfo.get() 217 assertThat(launchedTarget).isNotNull() 218 val launchedIntent = launchedTarget!!.resolvedIntent 219 assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND) 220 assertThat(launchedIntent.type).isEqualTo(DEFAULT_MIME_TYPE) 221 assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, IMAGE_ACTIVITY)) 222 } 223 224 @Test 225 fun test_changeSelectedItem_newlySelectedItemShared() { 226 val launchedTargetInfo = AtomicReference<TargetInfo?>() 227 with(ChooserActivityOverrideData.getInstance()) { 228 onSafelyStartInternalCallback = 229 Function<TargetInfo, Boolean> { targetInfo -> 230 launchedTargetInfo.set(targetInfo) 231 true 232 } 233 } 234 val videoMimeType = "video/mp4" 235 val mimeTypes = mapOf(1 to videoMimeType) 236 setBitmaps(mimeTypes) 237 fakeCursorResolver.setUris(count = 3, startPosition = 0, mimeTypes) 238 launchActivityWithComposeTestEnv(makeItemUri("0", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { 239 scrollToPosition(0) 240 tapOnItem(makeItemUri("0", DEFAULT_MIME_TYPE)) 241 scrollToPosition(1) 242 tapOnItem(makeItemUri("1", videoMimeType)) 243 selectTarget(VIDEO_ACTIVITY_LABEL) 244 } 245 246 val launchedTarget = launchedTargetInfo.get() 247 assertThat(launchedTarget).isNotNull() 248 val launchedIntent = launchedTarget!!.resolvedIntent 249 assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND) 250 assertThat(launchedIntent.type).isEqualTo(videoMimeType) 251 assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, VIDEO_ACTIVITY)) 252 } 253 254 @Test 255 fun test_selectAllItems_allItemsShared() { 256 val launchedTargetInfo = AtomicReference<TargetInfo?>() 257 with(ChooserActivityOverrideData.getInstance()) { 258 onSafelyStartInternalCallback = 259 Function<TargetInfo, Boolean> { targetInfo -> 260 launchedTargetInfo.set(targetInfo) 261 true 262 } 263 } 264 val videoMimeType = "video/mp4" 265 val mimeTypes = mapOf(1 to videoMimeType) 266 setBitmaps(mimeTypes) 267 fakeCursorResolver.setUris(3, 0, mimeTypes) 268 launchActivityWithComposeTestEnv(makeItemUri("0", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { 269 scrollToPosition(1) 270 tapOnItem(makeItemUri("1", videoMimeType)) 271 scrollToPosition(2) 272 tapOnItem(makeItemUri("2", DEFAULT_MIME_TYPE)) 273 selectTarget(ALL_MEDIA_ACTIVITY_LABEL) 274 } 275 276 val launchedTarget = launchedTargetInfo.get() 277 assertThat(launchedTarget).isNotNull() 278 val launchedIntent = launchedTarget!!.resolvedIntent 279 assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND_MULTIPLE) 280 assertThat(launchedIntent.type).isEqualTo("*/*") 281 assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, ALL_MEDIA_ACTIVITY)) 282 } 283 284 private fun setBitmaps(mimeTypes: Map<Int, String>) { 285 arrayOf(Color.RED, Color.GREEN, Color.BLUE).forEachIndexed { i, color -> 286 fakeImageLoader.setBitmap( 287 makeItemUri(i.toString(), mimeTypes.getOrDefault(i, DEFAULT_MIME_TYPE)), 288 createBitmap(100, 100, color), 289 ) 290 } 291 } 292 293 private fun launchActivityWithComposeTestEnv( 294 initialItem: Uri, 295 mimeType: String, 296 block: AndroidComposeUiTest<ChooserWrapperActivity>.() -> Unit, 297 ) { 298 val sendIntent = 299 Intent().apply { 300 action = Intent.ACTION_SEND 301 putExtra(Intent.EXTRA_STREAM, initialItem) 302 addCategory(TEST_TARGET_CATEGORY) 303 type = mimeType 304 clipData = ClipData("test", arrayOf(mimeType), ClipData.Item(initialItem)) 305 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 306 } 307 308 val chooserIntent = 309 Intent.createChooser(sendIntent, null).apply { 310 component = 311 ComponentName( 312 "com.android.intentresolver.tests", 313 "com.android.intentresolver.ChooserWrapperActivity", 314 ) 315 putExtra( 316 Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, 317 Uri.parse("content://com.android.intentresolver.test.additional"), 318 ) 319 putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false) 320 putExtra(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, 0) 321 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 322 } 323 val activityRef = AtomicReference<ChooserWrapperActivity?>() 324 val composeTestEnv = AndroidComposeUiTestEnvironment { 325 requireNotNull(activityRef.get()) { "Activity was not launched" } 326 } 327 var scenario: ActivityScenario<ChooserWrapperActivity?>? = null 328 try { 329 composeTestEnv.runTest { 330 this@runTest.mainClock.autoAdvance = true 331 scenario = ActivityScenario.launch<ChooserWrapperActivity>(chooserIntent) 332 scenario.onActivity { activityRef.set(it) } 333 waitForIdle() 334 block() 335 } 336 } finally { 337 scenario?.close() 338 } 339 } 340 341 private fun AndroidComposeUiTest<ChooserWrapperActivity>.tapOnItem(uri: Uri) { 342 onNodeWithTag(uri.toString()).performClick() 343 waitForIdle() 344 } 345 346 private fun AndroidComposeUiTest<ChooserWrapperActivity>.scrollToPosition(position: Int) { 347 onNode(hasScrollToIndexAction()).performScrollToIndex(position) 348 waitForIdle() 349 } 350 351 private fun AndroidComposeUiTest<ChooserWrapperActivity>.selectTarget(name: String) { 352 onView( 353 allOf( 354 withId(R.id.item), 355 ViewMatchers.hasDescendant(withText(name)), 356 ViewMatchers.isEnabled(), 357 ) 358 ) 359 .perform(click()) 360 waitForIdle() 361 } 362 363 private fun mockListController(resolverListController: ResolverListController) { 364 resolverListController.stub { 365 on { 366 getResolversForIntentAsUser(anyBoolean(), anyBoolean(), anyBoolean(), any(), any()) 367 } doAnswer 368 { invocation -> 369 fakeTargetResolutionLogic(invocation.getArgument<List<Intent>>(3)) 370 } 371 } 372 } 373 374 private fun fakeTargetResolutionLogic(intentList: List<Intent>): List<ResolvedComponentInfo> { 375 require(intentList.size == 1) { "Expected a single intent" } 376 val intent = intentList[0] 377 require( 378 intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE 379 ) { 380 "Expected send intent" 381 } 382 val mimeType = requireNotNull(intent.type) { "Expected intent with type" } 383 val (activity, label) = 384 when { 385 ClipDescription.compareMimeTypes(mimeType, "image/*") -> 386 IMAGE_ACTIVITY to IMAGE_ACTIVITY_LABEL 387 ClipDescription.compareMimeTypes(mimeType, "video/*") -> 388 VIDEO_ACTIVITY to VIDEO_ACTIVITY_LABEL 389 else -> ALL_MEDIA_ACTIVITY to ALL_MEDIA_ACTIVITY_LABEL 390 } 391 val componentName = ComponentName(PACKAGE, activity) 392 return listOf( 393 ResolvedComponentInfo( 394 componentName, 395 intent, 396 ResolveInfo().apply { 397 activityInfo = ResolverDataProvider.createActivityInfo(componentName) 398 targetUserId = UserHandle.USER_CURRENT 399 userHandle = PERSONAL_USER_HANDLE 400 nonLocalizedLabel = label 401 }, 402 ) 403 ) 404 } 405 406 companion object { 407 private val PERSONAL_USER_HANDLE: UserHandle = 408 InstrumentationRegistry.getInstrumentation().targetContext.getUser() 409 410 private val PERSONAL_USER = User(PERSONAL_USER_HANDLE.identifier, User.Role.PERSONAL) 411 } 412 } 413