summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
author Andrey Yepin <ayepin@google.com> 2025-02-14 16:25:23 -0800
committer Andrey Yepin <ayepin@google.com> 2025-02-24 17:23:20 -0800
commitc2af4f1653068e310db88613392676dbca2d5d69 (patch)
treee0ca3812805ec2dd7188a15a4c466948475aafe7 /tests
parent3834835c0078582c73795b8cf7a72d7d0458e275 (diff)
Some initial shareousel integration tests.
Set debug tags for shareousel items to allow items be targeted by the tests; Bug: 396745989 Test: manual a11y smoke test; manual functionality smoke test. Flag: EXEMPT trivial refactoring; tests only. Change-Id: Ic3c1f14190adea1502ac2cc24b229502c3f0d18d
Diffstat (limited to 'tests')
-rw-r--r--tests/activity/Android.bp2
-rw-r--r--tests/activity/AndroidManifest.xml6
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java1
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityShareouselTest.kt412
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityTest.java59
-rw-r--r--tests/activity/src/com/android/intentresolver/TestContentProvider.kt28
-rw-r--r--tests/activity/src/com/android/intentresolver/TestUtils.kt61
-rw-r--r--tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/FakePayloadToggleCursorResolver.kt67
-rw-r--r--tests/activity/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/FakeSelectionChangeCallback.kt25
9 files changed, 603 insertions, 58 deletions
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
+}