diff options
author | 2025-02-25 09:04:06 -0800 | |
---|---|---|
committer | 2025-02-25 09:04:06 -0800 | |
commit | 2ed251235b216315eb062f671a7722c592942110 (patch) | |
tree | 838673b8a2032f7568e7348ae60907a32bfa714c | |
parent | aa36a2386976bc20729acc2febe5475c53522cdb (diff) | |
parent | c2af4f1653068e310db88613392676dbca2d5d69 (diff) |
Merge "Some initial shareousel integration tests." into main
15 files changed, 610 insertions, 61 deletions
diff --git a/java/res/layout/chooser_grid_item.xml b/java/res/layout/chooser_grid_item.xml index 76d2e60f..b06cf1c9 100644 --- a/java/res/layout/chooser_grid_item.xml +++ b/java/res/layout/chooser_grid_item.xml @@ -18,7 +18,7 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" - android:id="@androidprv:id/item" + android:id="@+id/item" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/java/res/layout/chooser_grid_item_hover.xml b/java/res/layout/chooser_grid_item_hover.xml index 5e49c9fd..4f4cd38c 100644 --- a/java/res/layout/chooser_grid_item_hover.xml +++ b/java/res/layout/chooser_grid_item_hover.xml @@ -20,7 +20,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@androidprv:id/item" + android:id="@+id/item" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/PreviewImageLoader.kt index 1dc497b3..44d88c41 100644 --- a/java/src/com/android/intentresolver/contentpreview/PreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/PreviewImageLoader.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit -private const val TAG = "PayloadSelImageLoader" +private const val TAG = "ImageLoader" @Qualifier @MustBeDocumented @Retention(AnnotationRetention.BINARY) annotation class ThumbnailSize diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt index cba4600f..9a9a0821 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt @@ -66,6 +66,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -305,6 +306,7 @@ private fun ShareouselCard( targetState = bitmapLoadState, modifier = Modifier.semantics { this.contentDescription = contentDescription } + .testTag(viewModel.testTag) .clickableWithTapToScrollSupport( state = carouselState, index = index, diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt index b56aa365..85f278a6 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt @@ -34,4 +34,5 @@ data class ShareouselPreviewViewModel( val setSelected: suspend (Boolean) -> Unit, val aspectRatio: Float, val cursorPosition: Int, + val testTag: String, ) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt index 99053e0f..45e01e9d 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt @@ -154,6 +154,7 @@ object ShareouselViewModelModule { aspectRatio = key.aspectRatio, // only items with a final key has a known cursor position cursorPosition = if (key.key.isFinal) key.order else -1, + testTag = key.uri.toString(), ) }, ) diff --git a/tests/activity/Android.bp b/tests/activity/Android.bp index 2e66a84d..ef54d825 100644 --- a/tests/activity/Android.bp +++ b/tests/activity/Android.bp @@ -43,6 +43,8 @@ android_test { "androidx.test.ext.truth", "androidx.test.espresso.contrib", "androidx.test.espresso.core", + "androidx.compose.ui_ui-test-junit4", + "androidx.compose.ui_ui-test-manifest", "androidx.test.rules", "androidx.test.runner", "androidx.lifecycle_lifecycle-common-java8", diff --git a/tests/activity/AndroidManifest.xml b/tests/activity/AndroidManifest.xml index 00dbd78d..90cb3d92 100644 --- a/tests/activity/AndroidManifest.xml +++ b/tests/activity/AndroidManifest.xml @@ -22,12 +22,12 @@ <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/> <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> - <application android:name="dagger.hilt.android.testing.HiltTestApplication"> + <application android:name="dagger.hilt.android.testing.HiltTestApplication" + android:label="IntentResolver Tests"> <uses-library android:name="android.test.runner" /> <activity android:name="com.android.intentresolver.ChooserWrapperActivity" /> <activity android:name="com.android.intentresolver.ResolverWrapperActivity" /> - <activity android:name="com.android.intentresolver.ChooserWrapperActivity" /> - <activity android:name="com.android.intentresolver.ResolverWrapperActivity" /> + <provider android:authorities="com.android.intentresolver.tests" android:name="com.android.intentresolver.TestContentProvider" diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java index 15012a31..c583b056 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -49,7 +49,6 @@ public class ChooserActivityOverrideData { return sInstance; } public Function<TargetInfo, Boolean> onSafelyStartInternalCallback; - public Function<TargetInfo, Boolean> onSafelyStartCallback; public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> shortcutLoaderFactory = (userHandle, callback) -> null; public ChooserListController resolverListController; diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityShareouselTest.kt b/tests/activity/src/com/android/intentresolver/ChooserActivityShareouselTest.kt new file mode 100644 index 00000000..cf1d8c60 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityShareouselTest.kt @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.graphics.Color +import android.net.Uri +import android.os.UserHandle +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.provider.DeviceConfig +import androidx.compose.ui.test.AndroidComposeUiTest +import androidx.compose.ui.test.AndroidComposeUiTestEnvironment +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.hasScrollToIndexAction +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.TestContentProvider.Companion.makeItemUri +import com.android.intentresolver.chooser.TargetInfo +import com.android.intentresolver.contentpreview.ImageLoader +import com.android.intentresolver.contentpreview.ImageLoaderModule +import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.CursorResolver +import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.FakePayloadToggleCursorResolver +import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.FakePayloadToggleCursorResolver.Companion.DEFAULT_MIME_TYPE +import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle +import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggleCursorResolver +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.FakeSelectionChangeCallback +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback +import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallbackModule +import com.android.intentresolver.data.repository.FakeUserRepository +import com.android.intentresolver.data.repository.UserRepository +import com.android.intentresolver.data.repository.UserRepositoryModule +import com.android.intentresolver.inject.ApplicationUser +import com.android.intentresolver.inject.PackageManagerModule +import com.android.intentresolver.inject.ProfileParent +import com.android.intentresolver.platform.AppPredictionAvailable +import com.android.intentresolver.platform.AppPredictionModule +import com.android.intentresolver.platform.ImageEditor +import com.android.intentresolver.platform.ImageEditorModule +import com.android.intentresolver.shared.model.User +import com.android.intentresolver.tests.R +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags +import com.google.common.truth.Truth.assertThat +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import dagger.hilt.android.testing.UninstallModules +import java.util.Optional +import java.util.concurrent.atomic.AtomicReference +import java.util.function.Function +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.hamcrest.Matchers.allOf +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.stub + +private const val TEST_TARGET_CATEGORY = "com.android.intentresolver.tests.TEST_RECEIVER_CATEGORY" +private const val PACKAGE = "com.android.intentresolver.tests" +private const val IMAGE_ACTIVITY = "com.android.intentresolver.tests.ImageReceiverActivity" +private const val VIDEO_ACTIVITY = "com.android.intentresolver.tests.VideoReceiverActivity" +private const val ALL_MEDIA_ACTIVITY = "com.android.intentresolver.tests.AllMediaReceiverActivity" +private const val IMAGE_ACTIVITY_LABEL = "ImageActivity" +private const val VIDEO_ACTIVITY_LABEL = "VideoActivity" +private const val ALL_MEDIA_ACTIVITY_LABEL = "AllMediaActivity" + +/** + * Instrumentation tests for ChooserActivity. + * + * Legacy test suite migrated from framework CoreTests. + */ +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTestApi::class) +@HiltAndroidTest +@UninstallModules( + AppPredictionModule::class, + ImageEditorModule::class, + PackageManagerModule::class, + ImageLoaderModule::class, + UserRepositoryModule::class, + PayloadToggleCursorResolver.Binding::class, + SelectionChangeCallbackModule::class, +) +class ChooserActivityShareouselTest() { + @get:Rule(order = 0) + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @get:Rule(order = 1) val hiltAndroidRule: HiltAndroidRule = HiltAndroidRule(this) + + @Inject @ApplicationContext lateinit var context: Context + + @BindValue lateinit var packageManager: PackageManager + + private val fakeUserRepo = FakeUserRepository(listOf(PERSONAL_USER)) + + @BindValue val userRepository: UserRepository = fakeUserRepo + @AppPredictionAvailable @BindValue val appPredictionAvailable = false + + private val fakeImageLoader = FakeImageLoader() + + @BindValue val imageLoader: ImageLoader = fakeImageLoader + @BindValue + @ImageEditor + val imageEditor: Optional<ComponentName> = + Optional.ofNullable( + ComponentName.unflattenFromString( + "com.google.android.apps.messaging/.ui.conversationlist.ShareIntentActivity" + ) + ) + + @BindValue @ApplicationUser val applicationUser = PERSONAL_USER_HANDLE + + @BindValue @ProfileParent val profileParent = PERSONAL_USER_HANDLE + + private val fakeCursorResolver = FakePayloadToggleCursorResolver() + @BindValue + @PayloadToggle + val additionalContentCursorResolver: CursorResolver<CursorRow?> = fakeCursorResolver + + @BindValue val selectionChangeCallback: SelectionChangeCallback = FakeSelectionChangeCallback() + + @Before + fun setUp() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry.getInstrumentation().uiAutomation.adoptShellPermissionIdentity() + + cleanOverrideData() + + // Assign @Inject fields + hiltAndroidRule.inject() + + // Populate @BindValue dependencies using injected values. These fields contribute + // values to the dependency graph at activity launch time. This allows replacing + // arbitrary bindings per-test case if needed. + packageManager = context.packageManager + with(ChooserActivityOverrideData.getInstance()) { + personalUserHandle = PERSONAL_USER_HANDLE + mockListController(resolverListController) + } + } + + private fun setDeviceConfigProperty(propertyName: String, value: String) { + // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly + // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently + // configure in {@link #setup()}. + // TODO: is it really appropriate that this is always set with makeDefault=true? + val valueWasSet = + DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SYSTEMUI, + propertyName, + value, + true, /* makeDefault */ + ) + check(valueWasSet) { "Could not set $propertyName to $value" } + } + + private fun cleanOverrideData() { + ChooserActivityOverrideData.getInstance().reset() + + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + true.toString(), + ) + } + + @Test + fun test_shareInitiallySelectedItem_initiallySelectedItemShared() { + val launchedTargetInfo = AtomicReference<TargetInfo?>() + with(ChooserActivityOverrideData.getInstance()) { + onSafelyStartInternalCallback = + Function<TargetInfo, Boolean> { targetInfo -> + launchedTargetInfo.set(targetInfo) + true + } + } + val mimeTypes = emptyMap<Int, String>() + setBitmaps(mimeTypes) + fakeCursorResolver.setUris(count = 3, startPosition = 1, mimeTypes) + launchActivityWithComposeTestEnv(makeItemUri("1", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { + selectTarget(IMAGE_ACTIVITY_LABEL) + } + + val launchedTarget = launchedTargetInfo.get() + assertThat(launchedTarget).isNotNull() + val launchedIntent = launchedTarget!!.resolvedIntent + assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND) + assertThat(launchedIntent.type).isEqualTo(DEFAULT_MIME_TYPE) + assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, IMAGE_ACTIVITY)) + } + + @Test + fun test_changeSelectedItem_newlySelectedItemShared() { + val launchedTargetInfo = AtomicReference<TargetInfo?>() + with(ChooserActivityOverrideData.getInstance()) { + onSafelyStartInternalCallback = + Function<TargetInfo, Boolean> { targetInfo -> + launchedTargetInfo.set(targetInfo) + true + } + } + val videoMimeType = "video/mp4" + val mimeTypes = mapOf(1 to videoMimeType) + setBitmaps(mimeTypes) + fakeCursorResolver.setUris(count = 3, startPosition = 0, mimeTypes) + launchActivityWithComposeTestEnv(makeItemUri("0", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { + scrollToPosition(0) + tapOnItem(makeItemUri("0", DEFAULT_MIME_TYPE)) + scrollToPosition(1) + tapOnItem(makeItemUri("1", videoMimeType)) + selectTarget(VIDEO_ACTIVITY_LABEL) + } + + val launchedTarget = launchedTargetInfo.get() + assertThat(launchedTarget).isNotNull() + val launchedIntent = launchedTarget!!.resolvedIntent + assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND) + assertThat(launchedIntent.type).isEqualTo(videoMimeType) + assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, VIDEO_ACTIVITY)) + } + + @Test + fun test_selectAllItems_allItemsShared() { + val launchedTargetInfo = AtomicReference<TargetInfo?>() + with(ChooserActivityOverrideData.getInstance()) { + onSafelyStartInternalCallback = + Function<TargetInfo, Boolean> { targetInfo -> + launchedTargetInfo.set(targetInfo) + true + } + } + val videoMimeType = "video/mp4" + val mimeTypes = mapOf(1 to videoMimeType) + setBitmaps(mimeTypes) + fakeCursorResolver.setUris(3, 0, mimeTypes) + launchActivityWithComposeTestEnv(makeItemUri("0", DEFAULT_MIME_TYPE), DEFAULT_MIME_TYPE) { + scrollToPosition(1) + tapOnItem(makeItemUri("1", videoMimeType)) + scrollToPosition(2) + tapOnItem(makeItemUri("2", DEFAULT_MIME_TYPE)) + selectTarget(ALL_MEDIA_ACTIVITY_LABEL) + } + + val launchedTarget = launchedTargetInfo.get() + assertThat(launchedTarget).isNotNull() + val launchedIntent = launchedTarget!!.resolvedIntent + assertThat(launchedIntent.action).isEqualTo(Intent.ACTION_SEND_MULTIPLE) + assertThat(launchedIntent.type).isEqualTo("*/*") + assertThat(launchedIntent.component).isEqualTo(ComponentName(PACKAGE, ALL_MEDIA_ACTIVITY)) + } + + private fun setBitmaps(mimeTypes: Map<Int, String>) { + arrayOf(Color.RED, Color.GREEN, Color.BLUE).forEachIndexed { i, color -> + fakeImageLoader.setBitmap( + makeItemUri(i.toString(), mimeTypes.getOrDefault(i, DEFAULT_MIME_TYPE)), + createBitmap(100, 100, color), + ) + } + } + + private fun launchActivityWithComposeTestEnv( + initialItem: Uri, + mimeType: String, + block: AndroidComposeUiTest<ChooserWrapperActivity>.() -> Unit, + ) { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, initialItem) + addCategory(TEST_TARGET_CATEGORY) + type = mimeType + clipData = ClipData("test", arrayOf(mimeType), ClipData.Item(initialItem)) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + val chooserIntent = + Intent.createChooser(sendIntent, null).apply { + component = + ComponentName( + "com.android.intentresolver.tests", + "com.android.intentresolver.ChooserWrapperActivity", + ) + putExtra( + Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, + Uri.parse("content://com.android.intentresolver.test.additional"), + ) + putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false) + putExtra(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, 0) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + val activityRef = AtomicReference<ChooserWrapperActivity?>() + val composeTestEnv = AndroidComposeUiTestEnvironment { + requireNotNull(activityRef.get()) { "Activity was not launched" } + } + var scenario: ActivityScenario<ChooserWrapperActivity?>? = null + try { + composeTestEnv.runTest { + this@runTest.mainClock.autoAdvance = true + scenario = ActivityScenario.launch<ChooserWrapperActivity>(chooserIntent) + scenario.onActivity { activityRef.set(it) } + waitForIdle() + block() + } + } finally { + scenario?.close() + } + } + + private fun AndroidComposeUiTest<ChooserWrapperActivity>.tapOnItem(uri: Uri) { + onNodeWithTag(uri.toString()).performClick() + waitForIdle() + } + + private fun AndroidComposeUiTest<ChooserWrapperActivity>.scrollToPosition(position: Int) { + onNode(hasScrollToIndexAction()).performScrollToIndex(position) + waitForIdle() + } + + private fun AndroidComposeUiTest<ChooserWrapperActivity>.selectTarget(name: String) { + onView( + allOf( + withId(R.id.item), + ViewMatchers.hasDescendant(withText(name)), + ViewMatchers.isEnabled(), + ) + ) + .perform(click()) + waitForIdle() + } + + private fun mockListController(resolverListController: ResolverListController) { + resolverListController.stub { + on { + getResolversForIntentAsUser(anyBoolean(), anyBoolean(), anyBoolean(), any(), any()) + } doAnswer + { invocation -> + fakeTargetResolutionLogic(invocation.getArgument<List<Intent>>(3)) + } + } + } + + private fun fakeTargetResolutionLogic(intentList: List<Intent>): List<ResolvedComponentInfo> { + require(intentList.size == 1) { "Expected a single intent" } + val intent = intentList[0] + require( + intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE + ) { + "Expected send intent" + } + val mimeType = requireNotNull(intent.type) { "Expected intent with type" } + val (activity, label) = + when { + ClipDescription.compareMimeTypes(mimeType, "image/*") -> + IMAGE_ACTIVITY to IMAGE_ACTIVITY_LABEL + ClipDescription.compareMimeTypes(mimeType, "video/*") -> + VIDEO_ACTIVITY to VIDEO_ACTIVITY_LABEL + else -> ALL_MEDIA_ACTIVITY to ALL_MEDIA_ACTIVITY_LABEL + } + val componentName = ComponentName(PACKAGE, activity) + return listOf( + ResolvedComponentInfo( + componentName, + intent, + ResolveInfo().apply { + activityInfo = ResolverDataProvider.createActivityInfo(componentName) + targetUserId = UserHandle.USER_CURRENT + userHandle = PERSONAL_USER_HANDLE + nonLocalizedLabel = label + }, + ) + ) + } + + companion object { + private val PERSONAL_USER_HANDLE: UserHandle = + InstrumentationRegistry.getInstrumentation().targetContext.getUser() + + private val PERSONAL_USER = User(PERSONAL_USER_HANDLE.identifier, User.Role.PERSONAL) + } +} diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java index 1592f1de..6f80c8f6 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java @@ -38,6 +38,7 @@ import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_F import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; import static com.android.intentresolver.MatcherUtils.first; +import static com.android.intentresolver.TestUtils.createSendImageIntent; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -79,9 +80,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Icon; @@ -2850,19 +2849,6 @@ public class ChooserActivityTest { return sendIntent; } - private Intent createSendImageIntent(Uri imageThumbnail) { - Intent sendIntent = new Intent(); - sendIntent.setAction(Intent.ACTION_SEND); - sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail); - sendIntent.setType("image/png"); - if (imageThumbnail != null) { - ClipData.Item clipItem = new ClipData.Item(imageThumbnail); - sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); - } - - return sendIntent; - } - private Uri createTestContentProviderUri( @Nullable String mimeType, @Nullable String streamType) { return createTestContentProviderUri(mimeType, streamType, 0); @@ -2870,22 +2856,11 @@ public class ChooserActivityTest { private Uri createTestContentProviderUri( @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) { - String packageName = - InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); - Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png") - .buildUpon(); - if (mimeType != null) { - builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType); - } - if (streamType != null) { - builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType); - } - if (streamTypeTimeout > 0) { - builder.appendQueryParameter( - TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT, - Long.toString(streamTypeTimeout)); - } - return builder.build(); + return TestContentProvider.makeItemUri( + "image.png", + mimeType, + streamType == null ? new String[0] : new String[] { streamType }, + streamTypeTimeout); } private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) { @@ -3030,29 +3005,11 @@ public class ChooserActivityTest { Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); width = bounds.width() + 200; } - return createBitmap(width, 100, bgColor); + return TestUtils.createBitmap(width, 100, bgColor); } private Bitmap createBitmap(int width, int height) { - return createBitmap(width, height, Color.RED); - } - - private Bitmap createBitmap(int width, int height, int bgColor) { - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - Paint paint = new Paint(); - paint.setColor(bgColor); - paint.setStyle(Paint.Style.FILL); - canvas.drawPaint(paint); - - paint.setColor(Color.WHITE); - paint.setAntiAlias(true); - paint.setTextSize(14.f); - paint.setTextAlign(Paint.Align.CENTER); - canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint); - - return bitmap; + return TestUtils.createBitmap(width, height, Color.RED); } private List<ShareShortcutInfo> createShortcuts(Context context) { diff --git a/tests/activity/src/com/android/intentresolver/TestContentProvider.kt b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt index 426f9af2..dcd5888c 100644 --- a/tests/activity/src/com/android/intentresolver/TestContentProvider.kt +++ b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt @@ -27,7 +27,7 @@ class TestContentProvider : ContentProvider() { projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, - sortOrder: String? + sortOrder: String?, ): Cursor? = null override fun getType(uri: Uri): String? = @@ -44,7 +44,7 @@ class TestContentProvider : ContentProvider() { Thread.currentThread().interrupt() } } - return runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE)?.let { arrayOf(it) } } + return runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE)?.split(",")?.toTypedArray() } .getOrNull() } @@ -56,7 +56,7 @@ class TestContentProvider : ContentProvider() { uri: Uri, values: ContentValues?, selection: String?, - selectionArgs: Array<out String>? + selectionArgs: Array<out String>?, ): Int = 0 override fun onCreate(): Boolean = true @@ -65,5 +65,27 @@ class TestContentProvider : ContentProvider() { const val PARAM_MIME_TYPE = "mimeType" const val PARAM_STREAM_TYPE = "streamType" const val PARAM_STREAM_TYPE_TIMEOUT = "streamTypeTo" + + @JvmStatic + @JvmOverloads + fun makeItemUri( + name: String, + mimeType: String?, + streamTypes: Array<String> = emptyArray(), + timeout: Long = 0L, + ): Uri = + Uri.parse("content://com.android.intentresolver.tests/$name") + .buildUpon() + .appendQueryParameter(PARAM_MIME_TYPE, mimeType) + .apply { + mimeType?.let { appendQueryParameter(PARAM_MIME_TYPE, it) } + if (streamTypes.isNotEmpty()) { + appendQueryParameter(PARAM_STREAM_TYPE, streamTypes.joinToString(",")) + } + if (timeout > 0) { + appendQueryParameter(PARAM_STREAM_TYPE_TIMEOUT, timeout.toString()) + } + } + .build() } } diff --git a/tests/activity/src/com/android/intentresolver/TestUtils.kt b/tests/activity/src/com/android/intentresolver/TestUtils.kt new file mode 100644 index 00000000..18dee644 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/TestUtils.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("TestUtils") + +package com.android.intentresolver + +import android.content.ClipData +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.net.Uri + +@JvmOverloads +fun createSendImageIntent(imageThumbnail: Uri?, mimeType: String = "image/png") = + Intent().apply { + setAction(Intent.ACTION_SEND) + putExtra(Intent.EXTRA_STREAM, imageThumbnail) + setType(mimeType) + if (imageThumbnail != null) { + val clipItem = ClipData.Item(imageThumbnail) + clipData = ClipData("Clip Label", arrayOf<String>(mimeType), clipItem) + } + } + +fun createBitmap(width: Int, height: Int, bgColor: Int): Bitmap { + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val paint = + Paint().apply { + setColor(bgColor) + style = Paint.Style.FILL + } + canvas.drawPaint(paint) + + with(paint) { + setColor(Color.WHITE) + isAntiAlias = true + textSize = 14f + textAlign = Paint.Align.CENTER + } + canvas.drawText("Hi!", (width / 2f), (height / 2f), paint) + + return bitmap +} diff --git a/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/FakePayloadToggleCursorResolver.kt b/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/FakePayloadToggleCursorResolver.kt new file mode 100644 index 00000000..ded9dce0 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/FakePayloadToggleCursorResolver.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor + +import android.database.MatrixCursor +import android.net.Uri +import android.os.Bundle +import android.service.chooser.AdditionalContentContract +import com.android.intentresolver.TestContentProvider +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.CursorRow +import com.android.intentresolver.util.cursor.CursorView +import com.android.intentresolver.util.cursor.viewBy + +class FakePayloadToggleCursorResolver : CursorResolver<CursorRow?> { + private val uris = mutableListOf<Uri>() + private var startPosition = -1 + + fun setUris(count: Int, startPosition: Int, mimeTypes: Map<Int, String> = emptyMap()) { + uris.clear() + this.startPosition = startPosition + for (i in 0 until count) { + uris.add( + TestContentProvider.makeItemUri( + i.toString(), + mimeTypes.getOrDefault(i, DEFAULT_MIME_TYPE), + ) + ) + } + } + + override suspend fun getCursor(): CursorView<CursorRow?>? { + val cursor = MatrixCursor(arrayOf(AdditionalContentContract.Columns.URI)) + for (uri in uris) { + cursor.addRow(arrayOf(uri.toString())) + } + if (startPosition >= 0) { + var cursorExtras = cursor.extras + cursorExtras = + if (cursorExtras == null) { + Bundle() + } else { + Bundle(cursorExtras) + } + cursorExtras.putInt(AdditionalContentContract.CursorExtraKeys.POSITION, startPosition) + cursor.extras = cursorExtras + } + return cursor.viewBy { CursorRow(Uri.parse(getString(0)), null, position) } + } + + companion object { + const val DEFAULT_MIME_TYPE = "image/png" + } +} diff --git a/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/FakeSelectionChangeCallback.kt b/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/FakeSelectionChangeCallback.kt new file mode 100644 index 00000000..ad095677 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/FakeSelectionChangeCallback.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview.payloadtoggle.domain.update + +import android.content.Intent +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate + +/** Fake no-op [SelectionChangeCallback]. */ +class FakeSelectionChangeCallback : SelectionChangeCallback { + override suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? = null +} |