• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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