diff options
| author | 2023-08-14 15:42:50 -0700 | |
|---|---|---|
| committer | 2023-08-14 15:42:50 -0700 | |
| commit | 80bfff1f0eef6db4e061d9892450737e110bad59 (patch) | |
| tree | e586dfb61c90c1aec759f551cbac857895edaf83 /java/tests | |
| parent | a0c4477fdbbae9857ead667a5227c3d93d9d3167 (diff) | |
| parent | 5e86e4846a4326b05fa747eaf7f1ba0fa89cd623 (diff) | |
Merge Android U (ab/10368041)
Bug: 291102124
Merged-In: If40fe5329a6f3835d1edaebdef2ff47a947a9943
Change-Id: Ide948ae0ca758570ba7dae69fdcaa6c066522a20
Diffstat (limited to 'java/tests')
31 files changed, 2935 insertions, 909 deletions
diff --git a/java/tests/Android.bp b/java/tests/Android.bp index 4e835ec8..c381d0a8 100644 --- a/java/tests/Android.bp +++ b/java/tests/Android.bp @@ -29,12 +29,10 @@ android_test { "androidx.lifecycle_lifecycle-runtime-ktx", "truth-prebuilt", "testables", - "testng", "kotlinx_coroutines_test", ], test_suites: ["general-tests"], sdk_version: "core_platform", - platform_apis: true, compile_multilib: "both", dont_merge_manifests: true, diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml index 306eccb9..05830c4c 100644 --- a/java/tests/AndroidManifest.xml +++ b/java/tests/AndroidManifest.xml @@ -29,6 +29,10 @@ <uses-library android:name="android.test.runner" /> <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" + android:grantUriPermissions="true" /> </application> <instrumentation android:name="android.testing.TestableInstrumentation" diff --git a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt b/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt new file mode 100644 index 00000000..a17a560c --- /dev/null +++ b/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 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.os.UserHandle + +import com.google.common.truth.Truth.assertThat + +import org.junit.Test + +class AnnotatedUserHandlesTest { + + @Test + fun testBasicProperties() { // Fields that are reflected back w/o logic. + val info = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(42) + .setUserHandleSharesheetLaunchedAs(UserHandle.of(116)) + .setPersonalProfileUserHandle(UserHandle.of(117)) + .setWorkProfileUserHandle(UserHandle.of(118)) + .setCloneProfileUserHandle(UserHandle.of(119)) + .build() + + assertThat(info.userIdOfCallingApp).isEqualTo(42) + assertThat(info.userHandleSharesheetLaunchedAs.identifier).isEqualTo(116) + assertThat(info.personalProfileUserHandle.identifier).isEqualTo(117) + assertThat(info.workProfileUserHandle.identifier).isEqualTo(118) + assertThat(info.cloneProfileUserHandle.identifier).isEqualTo(119) + } + + @Test + fun testWorkTabInitiallySelectedWhenLaunchedFromWorkProfile() { + val info = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(42) + .setPersonalProfileUserHandle(UserHandle.of(101)) + .setWorkProfileUserHandle(UserHandle.of(202)) + .setUserHandleSharesheetLaunchedAs(UserHandle.of(202)) + .build() + + assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(202) + } + + @Test + fun testPersonalTabInitiallySelectedWhenLaunchedFromPersonalProfile() { + val info = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(42) + .setPersonalProfileUserHandle(UserHandle.of(101)) + .setWorkProfileUserHandle(UserHandle.of(202)) + .setUserHandleSharesheetLaunchedAs(UserHandle.of(101)) + .build() + + assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101) + } + + @Test + fun testPersonalTabInitiallySelectedWhenLaunchedFromOtherProfile() { + val info = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(42) + .setPersonalProfileUserHandle(UserHandle.of(101)) + .setWorkProfileUserHandle(UserHandle.of(202)) + .setUserHandleSharesheetLaunchedAs(UserHandle.of(303)) + .build() + + assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101) + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt index af134fcd..d72c9aa6 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -29,7 +29,6 @@ import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.android.intentresolver.flags.FeatureFlagRepository -import com.android.intentresolver.flags.Flags import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat import org.junit.After @@ -50,6 +49,7 @@ class ChooserActionFactoryTest { private val logger = mock<ChooserActivityLogger>() private val flags = mock<FeatureFlagRepository>() private val actionLabel = "Action label" + private val modifyShareLabel = "Modify share" private val testAction = "com.android.intentresolver.testaction" private val countdown = CountDownLatch(1) private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() { @@ -69,7 +69,6 @@ class ChooserActionFactoryTest { @Before fun setup() { - whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(true) context.registerReceiver(testReceiver, IntentFilter(testAction)) } @@ -104,18 +103,11 @@ class ChooserActionFactoryTest { } @Test - fun testNoModifyShareAction_flagDisabled() { - whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(false) - val factory = createFactory(includeModifyShare = true) - - assertThat(factory.modifyShareAction).isNull() - } - - @Test fun testModifyShareAction() { val factory = createFactory(includeModifyShare = true) - factory.modifyShareAction!!.run() + val action = factory.modifyShareAction ?: error("Modify share action should not be null") + action.onClicked.run() Mockito.verify(logger).logActionSelected( eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) @@ -137,13 +129,17 @@ class ChooserActionFactoryTest { whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) if (includeModifyShare) { - whenever(chooserRequest.modifyShareAction).thenReturn(testPendingIntent) + val modifyShare = ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + modifyShareLabel, + testPendingIntent + ).build() + whenever(chooserRequest.modifyShareAction).thenReturn(modifyShare) } return ChooserActionFactory( context, chooserRequest, - flags, mock<ChooserIntegratedDeviceComponents>(), logger, Consumer<Boolean>{}, diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index f0c459e5..ce96ef63 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -24,12 +24,11 @@ import static org.mockito.Mockito.when; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.Bitmap; import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.shortcuts.ShortcutLoader; @@ -55,35 +54,35 @@ public class ChooserActivityOverrideData { @SuppressWarnings("Since15") public Function<PackageManager, PackageManager> createPackageManager; + public Function<TargetInfo, Boolean> onSafelyStartInternalCallback; public Function<TargetInfo, Boolean> onSafelyStartCallback; public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> shortcutLoaderFactory = (userHandle, callback) -> null; public ChooserActivity.ChooserListController resolverListController; public ChooserActivity.ChooserListController workResolverListController; public Boolean isVoiceInteraction; - public boolean isImageType; public Cursor resolverCursor; public boolean resolverForceException; - public Bitmap previewThumbnail; + public ImageLoader imageLoader; public ChooserActivityLogger chooserActivityLogger; public int alternateProfileSetting; public Resources resources; public UserHandle workProfileUserHandle; + public UserHandle cloneProfileUserHandle; + public UserHandle tabOwnerUserHandleForLaunch; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public Integer myUserId; public WorkProfileAvailabilityManager mWorkProfileAvailability; - public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public PackageManager packageManager; public FeatureFlagRepository featureFlagRepository; public void reset() { - onSafelyStartCallback = null; + onSafelyStartInternalCallback = null; isVoiceInteraction = null; createPackageManager = null; - previewThumbnail = null; - isImageType = false; + imageLoader = null; resolverCursor = null; resolverForceException = false; resolverListController = mock(ChooserActivity.ChooserListController.class); @@ -92,6 +91,8 @@ public class ChooserActivityOverrideData { alternateProfileSetting = 0; resources = null; workProfileUserHandle = null; + cloneProfileUserHandle = null; + tabOwnerUserHandleForLaunch = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; myUserId = null; @@ -122,13 +123,6 @@ public class ChooserActivityOverrideData { }; shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); - mMyUserIdProvider = new MyUserIdProvider() { - @Override - public int getMyUserId() { - return myUserId != null ? myUserId : UserHandle.myUserId(); - } - }; - mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) .thenAnswer(invocation -> hasCrossProfileIntents); diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt index 58f6b733..4612b430 100644 --- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -20,16 +20,17 @@ import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags +import android.os.UserHandle import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.ChooserListAdapter.LoadDirectShareIconTask import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.chooser.SelectableTargetInfo import com.android.intentresolver.chooser.TargetInfo +import com.android.intentresolver.icons.TargetDataLoader import com.android.internal.R import org.junit.Before import org.junit.Test @@ -39,43 +40,43 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class ChooserListAdapterTest { - private val packageManager = mock<PackageManager> { - whenever( - resolveActivity(any(), any<ResolveInfoFlags>()) - ).thenReturn(mock()) - } - private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val userHandle: UserHandle = + InstrumentationRegistry.getInstrumentation().targetContext.user + + private val packageManager = + mock<PackageManager> { + whenever(resolveActivity(any(), any<ResolveInfoFlags>())).thenReturn(mock()) + } + private val context = InstrumentationRegistry.getInstrumentation().context private val resolverListController = mock<ResolverListController>() private val chooserActivityLogger = mock<ChooserActivityLogger>() + private val mTargetDataLoader = mock<TargetDataLoader>() - private fun createChooserListAdapter( - taskProvider: (TargetInfo?) -> LoadDirectShareIconTask - ) = object : ChooserListAdapter( + private val testSubject by lazy { + ChooserListAdapter( context, emptyList(), emptyArray(), emptyList(), false, resolverListController, - null, + userHandle, Intent(), mock(), packageManager, chooserActivityLogger, mock(), - 0 - ) { - override fun createLoadDirectShareIconTask( - info: SelectableTargetInfo - ): LoadDirectShareIconTask = taskProvider(info) - } + 0, + null, + mTargetDataLoader + ) + } @Before fun setup() { // ChooserListAdapter reads DeviceConfig and needs a permission for that. - InstrumentationRegistry - .getInstrumentation() - .getUiAutomation() + InstrumentationRegistry.getInstrumentation() + .uiAutomation .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") } @@ -85,41 +86,56 @@ class ChooserListAdapterTest { val viewHolder = ResolverListAdapter.ViewHolder(view) view.tag = viewHolder val targetInfo = createSelectableTargetInfo() - val iconTask = mock<LoadDirectShareIconTask>() - val testSubject = createChooserListAdapter { iconTask } testSubject.onBindView(view, targetInfo, 0) - verify(iconTask, times(1)).loadIcon() + verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) } @Test - fun testOnlyOneTaskPerTarget() { + fun onBindView_DirectShareTargetIconAndLabelLoadedOnlyOnce() { val view = createView() val viewHolderOne = ResolverListAdapter.ViewHolder(view) view.tag = viewHolderOne val targetInfo = createSelectableTargetInfo() - val iconTaskOne = mock<LoadDirectShareIconTask>() - val testTaskProvider = mock<() -> LoadDirectShareIconTask> { - whenever(invoke()).thenReturn(iconTaskOne) - } - val testSubject = createChooserListAdapter { testTaskProvider.invoke() } testSubject.onBindView(view, targetInfo, 0) val viewHolderTwo = ResolverListAdapter.ViewHolder(view) view.tag = viewHolderTwo - whenever(testTaskProvider()).thenReturn(mock()) testSubject.onBindView(view, targetInfo, 0) - verify(iconTaskOne, times(1)).loadIcon() - verify(testTaskProvider, times(1)).invoke() + verify(mTargetDataLoader, times(1)).loadDirectShareIcon(any(), any(), any()) + } + + @Test + fun onBindView_AppTargetIconAndLabelLoadedOnlyOnce() { + val view = createView() + val viewHolderOne = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderOne + val targetInfo = + DisplayResolveInfo.newDisplayResolveInfo( + Intent(), + ResolverDataProvider.createResolveInfo(2, 0, userHandle), + null, + "extended info", + Intent(), + /* resolveInfoPresentationGetter= */ null + ) + testSubject.onBindView(view, targetInfo, 0) + + val viewHolderTwo = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderTwo + + testSubject.onBindView(view, targetInfo, 0) + + verify(mTargetDataLoader, times(1)).loadAppTargetIcon(any(), any(), any()) } private fun createSelectableTargetInfo(): TargetInfo = SelectableTargetInfo.newSelectableTargetInfo( /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(2, 0), + ResolverDataProvider.createResolveInfo(2, 0, userHandle), "label", "extended info", Intent(), @@ -128,7 +144,10 @@ class ChooserListAdapterTest { /* backupResolveInfo = */ mock(), /* resolvedIntent = */ Intent(), /* chooserTarget = */ createChooserTarget( - "Target", 0.5f, ComponentName("pkg", "Class"), "id-1" + "Target", + 0.5f, + ComponentName("pkg", "Class"), + "id-1" ), /* modifiedScore = */ 1f, /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1), diff --git a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt index 50c37c7f..bd355c86 100644 --- a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt @@ -16,46 +16,227 @@ package com.android.intentresolver -import android.content.Context +import android.app.Activity +import android.app.Application import android.content.Intent import android.content.IntentSender +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Message +import android.os.ResultReceiver +import androidx.lifecycle.Observer +import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.intentresolver.ChooserRefinementManager.RefinementCompletion +import com.android.intentresolver.chooser.ImmutableTargetInfo import com.android.intentresolver.chooser.TargetInfo +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mockito -import java.util.function.Consumer -import org.junit.Assert.assertEquals @RunWith(AndroidJUnit4::class) +@UiThreadTest class ChooserRefinementManagerTest { - @Test - fun testMaybeHandleSelection() { - val intentSender = mock<IntentSender>() - val refinementManager = ChooserRefinementManager( - mock<Context>(), - intentSender, - Consumer<TargetInfo>{}, - Runnable{}) - - val intents = listOf(Intent(Intent.ACTION_VIEW), Intent(Intent.ACTION_EDIT)) - val targetInfo = mock<TargetInfo>{ - whenever(allSourceIntents).thenReturn(intents) + private val refinementManager = ChooserRefinementManager() + private val intentSender = mock<IntentSender>() + private val application = mock<Application>() + private val exampleSourceIntents = + listOf(Intent(Intent.ACTION_VIEW), Intent(Intent.ACTION_EDIT)) + private val exampleTargetInfo = + ImmutableTargetInfo.newBuilder().setAllSourceIntents(exampleSourceIntents).build() + + private val completionObserver = + object : Observer<RefinementCompletion> { + val failureCountDown = CountDownLatch(1) + val successCountDown = CountDownLatch(1) + var latestTargetInfo: TargetInfo? = null + + override fun onChanged(completion: RefinementCompletion) { + if (completion.consume()) { + val targetInfo = completion.targetInfo + if (targetInfo == null) { + failureCountDown.countDown() + } else { + latestTargetInfo = targetInfo + successCountDown.countDown() + } + } + } } - refinementManager.maybeHandleSelection(targetInfo) + /** Synchronously executes post() calls. */ + private class FakeHandler(looper: Looper) : Handler(looper) { + override fun sendMessageAtTime(msg: Message, uptimeMillis: Long): Boolean { + dispatchMessage(msg) + return true + } + } + + @Before + fun setup() { + refinementManager.refinementCompletion.observeForever(completionObserver) + } + + @Test + fun testTypicalRefinementFlow() { + assertThat( + refinementManager.maybeHandleSelection( + exampleTargetInfo, + intentSender, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isTrue() val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - Mockito.verify(intentSender).sendIntent( - any(), eq(0), intentCaptor.capture(), eq(null), eq(null)) + Mockito.verify(intentSender) + .sendIntent(any(), eq(0), intentCaptor.capture(), eq(null), eq(null)) val intent = intentCaptor.value - assertEquals(intents[0], intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) + assertThat(intent?.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)) + .isEqualTo(exampleSourceIntents[0]) val alternates = - intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java) - assertEquals(1, alternates?.size) - assertEquals(intents[1], alternates?.get(0)) + intent?.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java) + assertThat(alternates?.size).isEqualTo(1) + assertThat(alternates?.get(0)).isEqualTo(exampleSourceIntents[1]) + + // Complete the refinement + val receiver = + intent?.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) + val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } + receiver?.send(Activity.RESULT_OK, bundle) + + assertThat(completionObserver.successCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() + assertThat(completionObserver.latestTargetInfo?.resolvedIntent?.action) + .isEqualTo(Intent.ACTION_VIEW) + } + + @Test + fun testRefinementCancelled() { + assertThat( + refinementManager.maybeHandleSelection( + exampleTargetInfo, + intentSender, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isTrue() + + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + Mockito.verify(intentSender) + .sendIntent(any(), eq(0), intentCaptor.capture(), eq(null), eq(null)) + + val intent = intentCaptor.value + + // Complete the refinement + val receiver = + intent?.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java) + val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, exampleSourceIntents[0]) } + receiver?.send(Activity.RESULT_CANCELED, bundle) + + assertThat(completionObserver.failureCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue() + } + + @Test + fun testMaybeHandleSelection_noSourceIntents() { + assertThat( + refinementManager.maybeHandleSelection( + ImmutableTargetInfo.newBuilder().build(), + intentSender, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isFalse() + } + + @Test + fun testMaybeHandleSelection_suspended() { + val targetInfo = + ImmutableTargetInfo.newBuilder() + .setAllSourceIntents(exampleSourceIntents) + .setIsSuspended(true) + .build() + + assertThat( + refinementManager.maybeHandleSelection( + targetInfo, + intentSender, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isFalse() + } + + @Test + fun testMaybeHandleSelection_noIntentSender() { + assertThat( + refinementManager.maybeHandleSelection( + exampleTargetInfo, + /* IntentSender */ null, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isFalse() + } + + @Test + fun testConfigurationChangeDuringRefinement() { + assertThat( + refinementManager.maybeHandleSelection( + exampleTargetInfo, + intentSender, + application, + FakeHandler(Looper.myLooper()) + ) + ) + .isTrue() + + refinementManager.onActivityStop(/* config changing = */ true) + refinementManager.onActivityResume() + + assertThat(completionObserver.failureCountDown.count).isEqualTo(1) + } + + @Test + fun testResumeDuringRefinement() { + assertThat( + refinementManager.maybeHandleSelection( + exampleTargetInfo, + intentSender, + application, + FakeHandler(Looper.myLooper()!!) + ) + ) + .isTrue() + + refinementManager.onActivityStop(/* config changing = */ false) + // Resume during refinement but not during a config change, so finish the activity. + refinementManager.onActivityResume() + + // Call should be synchronous, don't need to await for this one. + assertThat(completionObserver.failureCountDown.count).isEqualTo(0) + } + + @Test + fun testRefinementCompletion() { + val refinementCompletion = RefinementCompletion(exampleTargetInfo) + assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo) + assertThat(refinementCompletion.consume()).isTrue() + assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo) + + // can only consume once. + assertThat(refinementCompletion.consume()).isFalse() } } diff --git a/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt b/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt new file mode 100644 index 00000000..331d1c21 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 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.app.PendingIntent +import android.content.Intent +import android.graphics.drawable.Icon +import android.net.Uri +import android.service.chooser.ChooserAction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ChooserRequestParametersTest { + val flags = TestFeatureFlagRepository(mapOf()) + + @Test + fun testChooserActions() { + val actionCount = 3 + val intent = Intent(Intent.ACTION_SEND) + val actions = createChooserActions(actionCount) + val chooserIntent = + Intent(Intent.ACTION_CHOOSER).apply { + putExtra(Intent.EXTRA_INTENT, intent) + putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, actions) + } + val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) + assertThat(request.chooserActions).containsExactlyElementsIn(actions).inOrder() + } + + @Test + fun testChooserActions_empty() { + val intent = Intent(Intent.ACTION_SEND) + val chooserIntent = + Intent(Intent.ACTION_CHOOSER).apply { putExtra(Intent.EXTRA_INTENT, intent) } + val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) + assertThat(request.chooserActions).isEmpty() + } + + @Test + fun testChooserActions_tooMany() { + val intent = Intent(Intent.ACTION_SEND) + val chooserActions = createChooserActions(10) + val chooserIntent = + Intent(Intent.ACTION_CHOOSER).apply { + putExtra(Intent.EXTRA_INTENT, intent) + putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions) + } + + val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags) + + val expectedActions = chooserActions.sliceArray(0 until 5) + assertThat(request.chooserActions).containsExactlyElementsIn(expectedActions).inOrder() + } + + private fun createChooserActions(count: Int): Array<ChooserAction> { + return Array(count) { i -> createChooserAction("$i") } + } + + private fun createChooserAction(label: CharSequence): ChooserAction { + val icon = Icon.createWithContentUri("content://org.package.app/image") + val pendingIntent = + PendingIntent.getBroadcast( + InstrumentationRegistry.getInstrumentation().getTargetContext(), + 0, + Intent("TESTACTION"), + PendingIntent.FLAG_IMMUTABLE + ) + return ChooserAction.Builder(icon, label, pendingIntent).build() + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index d4ae666b..6ac6b6d3 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -29,14 +29,17 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; import android.os.UserHandle; +import androidx.lifecycle.ViewModelProvider; + import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -70,7 +73,8 @@ public class ChooserWrapperActivity UserHandle userHandle, Intent targetIntent, ChooserRequestParameters chooserRequest, - int maxTargetsPerRow) { + int maxTargetsPerRow, + TargetDataLoader targetDataLoader) { PackageManager packageManager = sOverrides.packageManager == null ? context.getPackageManager() : sOverrides.packageManager; @@ -80,14 +84,16 @@ public class ChooserWrapperActivity initialIntents, rList, filterLastUsed, - resolverListController, + createListController(userHandle), userHandle, targetIntent, this, packageManager, getChooserActivityLogger(), chooserRequest, - maxTargetsPerRow); + maxTargetsPerRow, + userHandle, + targetDataLoader); } @Override @@ -142,14 +148,6 @@ public class ChooserWrapperActivity } @Override - protected MyUserIdProvider createMyUserIdProvider() { - if (sOverrides.mMyUserIdProvider != null) { - return sOverrides.mMyUserIdProvider; - } - return super.createMyUserIdProvider(); - } - - @Override protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { if (sOverrides.mCrossProfileIntentsChecker != null) { return sOverrides.mCrossProfileIntentsChecker; @@ -166,12 +164,13 @@ public class ChooserWrapperActivity } @Override - public void safelyStartActivity(TargetInfo cti) { - if (sOverrides.onSafelyStartCallback != null - && sOverrides.onSafelyStartCallback.apply(cti)) { + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(cti)) { return; } - super.safelyStartActivity(cti); + super.safelyStartActivityInternal(cti, user, options); } @Override @@ -199,15 +198,10 @@ public class ChooserWrapperActivity } @Override - protected ImageLoader createPreviewImageLoader() { - return new TestPreviewImageLoader( - super.createPreviewImageLoader(), - () -> sOverrides.previewThumbnail); - } - - @Override - protected boolean isImageType(String mimeType) { - return sOverrides.isImageType; + protected ViewModelProvider.Factory createPreviewViewModelFactory() { + return TestContentPreviewViewModel.Companion.wrap( + super.createPreviewViewModelFactory(), + sOverrides.imageLoader); } @Override @@ -260,6 +254,14 @@ public class ChooserWrapperActivity } @Override + protected UserHandle getTabOwnerUserHandleForLaunch() { + if (sOverrides.tabOwnerUserHandleForLaunch == null) { + return super.getTabOwnerUserHandleForLaunch(); + } + return sOverrides.tabOwnerUserHandleForLaunch; + } + + @Override public Context createContextAsUser(UserHandle user, int flags) { // return the current context as a work profile doesn't really exist in these tests return getApplicationContext(); diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt deleted file mode 100644 index f327e19e..00000000 --- a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2023 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.ContentResolver -import android.content.Context -import android.content.res.Resources -import android.net.Uri -import android.util.Size -import androidx.lifecycle.Lifecycle -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineScheduler -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.mockito.Mockito.never -import org.mockito.Mockito.times -import org.mockito.Mockito.verify - -@OptIn(ExperimentalCoroutinesApi::class) -class ImagePreviewImageLoaderTest { - private val imageSize = Size(300, 300) - private val uriOne = Uri.parse("content://org.package.app/image-1.png") - private val uriTwo = Uri.parse("content://org.package.app/image-2.png") - private val contentResolver = mock<ContentResolver>() - private val resources = mock<Resources> { - whenever(getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)) - .thenReturn(imageSize.width) - } - private val context = mock<Context> { - whenever(this.resources).thenReturn(this@ImagePreviewImageLoaderTest.resources) - whenever(this.contentResolver).thenReturn(this@ImagePreviewImageLoaderTest.contentResolver) - } - private val scheduler = TestCoroutineScheduler() - private val lifecycleOwner = TestLifecycleOwner() - private val dispatcher = UnconfinedTestDispatcher(scheduler) - private val testSubject = ImagePreviewImageLoader( - context, lifecycleOwner.lifecycle, 1, dispatcher - ) - - @Before - fun setup() { - Dispatchers.setMain(dispatcher) - lifecycleOwner.state = Lifecycle.State.CREATED - } - - @After - fun cleanup() { - lifecycleOwner.state = Lifecycle.State.DESTROYED - Dispatchers.resetMain() - } - - @Test - fun test_prePopulate() = runTest { - testSubject.prePopulate(listOf(uriOne, uriTwo)) - - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) - - testSubject(uriOne) - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - } - - @Test - fun test_invoke_return_cached_image() = runTest { - testSubject(uriOne) - testSubject(uriOne) - - verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) - } - - @Test - fun test_invoke_old_records_evicted_from_the_cache() = runTest { - testSubject(uriOne) - testSubject(uriTwo) - testSubject(uriTwo) - testSubject(uriOne) - - verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) - verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) - } -} diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index ae1b99f8..7233fd3d 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -33,10 +33,11 @@ import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.fail; import android.content.Intent; import android.content.pm.ResolveInfo; @@ -55,7 +56,8 @@ import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.intentresolver.widget.ResolverDrawerLayout; -import com.android.internal.R; + +import com.google.android.collect.Lists; import org.junit.Before; import org.junit.Ignore; @@ -72,6 +74,9 @@ import java.util.List; */ @RunWith(AndroidJUnit4.class) public class ResolverActivityTest { + + private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app + .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser(); protected Intent getConcreteIntentForLaunch(Intent clientIntent) { clientIntent.setClass( androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(), @@ -98,26 +103,27 @@ public class ResolverActivityTest { @Test public void twoOptionsAndUserSelectsOne() throws InterruptedException { Intent sendIntent = createSendImageIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); onView(withText(toChoose.activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); @@ -127,19 +133,20 @@ public class ResolverActivityTest { @Test public void setMaxHeight() throws Exception { Intent sendIntent = createSendImageIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - final View viewPager = activity.findViewById(R.id.profile_pager); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); final int initialResolverHeight = viewPager.getHeight(); activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight = initialResolverHeight - 1; // Force a relayout @@ -153,7 +160,7 @@ public class ResolverActivityTest { activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight = initialResolverHeight + 1; // Force a relayout @@ -169,16 +176,18 @@ public class ResolverActivityTest { @Test public void setShowAtTopToTrue() throws Exception { Intent sendIntent = createSendImageIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - final View viewPager = activity.findViewById(R.id.profile_pager); - final View divider = activity.findViewById(R.id.divider); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final View divider = activity.findViewById(com.android.internal.R.id.divider); final RelativeLayout profileView = - (RelativeLayout) activity.findViewById(R.id.profile_button).getParent(); + (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button) + .getParent(); assertThat("Drawer should show at bottom by default", profileView.getBottom() + divider.getHeight() == viewPager.getTop() && profileView.getTop() > 0); @@ -186,7 +195,7 @@ public class ResolverActivityTest { activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); layout.setShowAtTop(true); }); waitForIdle(); @@ -198,7 +207,8 @@ public class ResolverActivityTest { @Test public void hasLastChosenActivity() throws Exception { Intent sendIntent = createSendImageIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); @@ -213,12 +223,12 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -226,32 +236,35 @@ public class ResolverActivityTest { @Test public void hasOtherProfileOneOption() throws Exception { List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); Intent sendIntent = createSendImageIntent(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the last used slot assertThat(activity.getAdapter().getCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = - createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10, + PERSONAL_USER_HANDLE); // We pick the first one as there is another one in the work profile side onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); @@ -261,34 +274,34 @@ public class ResolverActivityTest { public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; // Confirm that the button bar is disabled by default - onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = - createResolvedComponentsForTestWithOtherProfile(2); + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -300,7 +313,7 @@ public class ResolverActivityTest { // chosen activity. Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); @@ -308,28 +321,28 @@ public class ResolverActivityTest { .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; // Confirm that the button bar is disabled by default - onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); // Make a stable copy of the components as the original list may be modified List<ResolvedComponentInfo> stableCopy = - createResolvedComponentsForTestWithOtherProfile(2); + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -342,7 +355,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.tabs)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed())); } @Test @@ -352,18 +365,20 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed()))); } @Test public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10, + PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>(workResolvedComponentInfos)); Intent sendIntent = createSendImageIntent(); - markWorkProfileUserAvailable(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); @@ -376,8 +391,11 @@ public class ResolverActivityTest { @Test public void testWorkTab_workTabUsesExpectedAdapter() { List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); markWorkProfileUserAvailable(); @@ -393,11 +411,12 @@ public class ResolverActivityTest { @Test public void testWorkTab_personalTabUsesExpectedAdapter() { List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); - markWorkProfileUserAvailable(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); @@ -411,8 +430,10 @@ public class ResolverActivityTest { public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -429,13 +450,15 @@ public class ResolverActivityTest { public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -447,7 +470,7 @@ public class ResolverActivityTest { onView(first(allOf(withText(workResolvedComponentInfos.get(0) .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); @@ -459,8 +482,9 @@ public class ResolverActivityTest { throws InterruptedException { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -477,16 +501,17 @@ public class ResolverActivityTest { public void testWorkTab_headerIsVisibleInPersonalTab() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createOpenWebsiteIntent(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); - TextView headerText = activity.findViewById(R.id.title); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); String initialText = headerText.getText().toString(); - assertFalse(initialText.isEmpty(), "Header text is empty."); + assertFalse("Header text is empty.", initialText.isEmpty()); assertThat(headerText.getVisibility(), is(View.VISIBLE)); } @@ -494,14 +519,15 @@ public class ResolverActivityTest { public void testWorkTab_switchTabs_headerStaysSame() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createOpenWebsiteIntent(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); - TextView headerText = activity.findViewById(R.id.title); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); String initialText = headerText.getText().toString(); onView(withText(R.string.resolver_work_tab)) .perform(click()); @@ -519,13 +545,15 @@ public class ResolverActivityTest { throws InterruptedException { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -539,7 +567,7 @@ public class ResolverActivityTest { .getResolveInfoAt(0).activityInfo.applicationInfo.name), isDisplayed()))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); @@ -551,9 +579,11 @@ public class ResolverActivityTest { markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.hasCrossProfileIntents = false; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -563,7 +593,7 @@ public class ResolverActivityTest { waitForIdle(); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_cross_profile_blocked)) @@ -575,9 +605,11 @@ public class ResolverActivityTest { markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.isQuietModeEnabled = true; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -585,7 +617,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -598,16 +630,16 @@ public class ResolverActivityTest { public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -620,9 +652,9 @@ public class ResolverActivityTest { public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -631,7 +663,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -644,9 +676,9 @@ public class ResolverActivityTest { public void testMiniResolver() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); // Personal profile only has a browser personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); @@ -655,16 +687,16 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed())); } @Test public void testMiniResolver_noCurrentProfileTarget() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -678,7 +710,8 @@ public class ResolverActivityTest { private void assertNotMiniResolver() { try { - onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.open_cross_profile)) + .check(matches(isDisplayed())); } catch (NoMatchingViewException e) { return; } @@ -689,9 +722,9 @@ public class ResolverActivityTest { public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -699,7 +732,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -709,27 +742,29 @@ public class ResolverActivityTest { } @Test - public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() { + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.hasCrossProfileIntents = false; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; mActivityRule.launchActivity(sendIntent); waitForIdle(); - assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); + assertNull(chosen[0]); } @Test @@ -740,14 +775,14 @@ public class ResolverActivityTest { // chosen activity. Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = - createResolvedComponentsForTest(2); + createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); when(sOverrides.resolverListController.getLastChosen()) .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the last used slot @@ -756,6 +791,200 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); } + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markCloneProfileUserAvailable(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 2, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + + onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled())); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + } + + @Test + public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser() + throws Exception { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(first(allOf(withText(personalResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser() + throws Exception { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers() + throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + List<UserHandle> result = activity + .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE); + + assertThat(result.containsAll(Lists.newArrayList(PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle)), is(true)); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -771,36 +1000,56 @@ public class ResolverActivityTest { return sendIntent; } - private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); } return infoList; } private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( - int numberOfResults) { + int numberOfResults, + UserHandle resolvedForUser) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + resolvedForUser)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } } return infoList; } private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( - int numberOfResults, int userId) { + int numberOfResults, int userId, UserHandle resolvedForUser) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + resolvedForUser)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } } return infoList; @@ -819,6 +1068,10 @@ public class ResolverActivityTest { setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); } + private void markCloneProfileUserAvailable() { + ResolverWrapperActivity.sOverrides.cloneProfileUserHandle = UserHandle.of(11); + } + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index b6b32b5a..688dd867 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -43,6 +43,14 @@ public class ResolverDataProvider { createResolveInfo(i, UserHandle.USER_CURRENT)); } + static ResolvedComponentInfo createResolvedComponentInfo(int i, + UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, UserHandle.USER_CURRENT, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfo( ComponentName componentName, Intent intent) { return new ResolvedComponentInfo( @@ -51,6 +59,14 @@ public class ResolverDataProvider { createResolveInfo(componentName, UserHandle.USER_CURRENT)); } + static ResolvedComponentInfo createResolvedComponentInfo( + ComponentName componentName, Intent intent, UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + componentName, + intent, + createResolveInfo(componentName, UserHandle.USER_CURRENT, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { return new ResolvedComponentInfo( createComponentName(i), @@ -58,6 +74,14 @@ public class ResolverDataProvider { createResolveInfo(i, USER_SOMEONE_ELSE)); } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, + UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, USER_SOMEONE_ELSE, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) { return new ResolvedComponentInfo( createComponentName(i), @@ -65,6 +89,14 @@ public class ResolverDataProvider { createResolveInfo(i, userId)); } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, + int userId, UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, userId, resolvedForUser)); + } + public static ComponentName createComponentName(int i) { final String name = "component" + i; return new ComponentName("foo.bar." + name, name); @@ -76,6 +108,13 @@ public class ResolverDataProvider { resolveInfo.targetUserId = userId; return resolveInfo; } + public static ResolveInfo createResolveInfo(int i, int userId, UserHandle resolvedForUser) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(i); + resolveInfo.targetUserId = userId; + resolveInfo.userHandle = resolvedForUser; + return resolveInfo; + } public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) { final ResolveInfo resolveInfo = new ResolveInfo(); @@ -84,6 +123,15 @@ public class ResolverDataProvider { return resolveInfo; } + public static ResolveInfo createResolveInfo(ComponentName componentName, int userId, + UserHandle resolvedForUser) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(componentName); + resolveInfo.targetUserId = userId; + resolveInfo.userHandle = resolvedForUser; + return resolveInfo; + } + static ActivityInfo createActivityInfo(int i) { ActivityInfo ai = new ActivityInfo(); ai.name = "activity_name" + i; diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java index d67b73af..401ede26 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -21,19 +21,27 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import android.app.usage.UsageStatsManager; +import android.annotation.Nullable; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.os.UserHandle; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.test.espresso.idling.CountingIdlingResource; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.icons.TargetDataLoader; import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; /* @@ -41,7 +49,9 @@ import java.util.function.Function; */ public class ResolverWrapperActivity extends ResolverActivity { static final OverrideData sOverrides = new OverrideData(); - private UsageStatsManager mUsm; + + private final CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); public ResolverWrapperActivity() { super(/* isIntentPicker= */ true); @@ -54,11 +64,20 @@ public class ResolverWrapperActivity extends ResolverActivity { return 1234; } + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + @Override - public ResolverListAdapter createResolverListAdapter(Context context, - List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, - boolean filterLastUsed, UserHandle userHandle) { - return new ResolverWrapperAdapter( + public ResolverListAdapter createResolverListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle, + TargetDataLoader targetDataLoader) { + return new ResolverListAdapter( context, payloadIntents, initialIntents, @@ -67,15 +86,9 @@ public class ResolverWrapperActivity extends ResolverActivity { createListController(userHandle), userHandle, payloadIntents.get(0), // TODO: extract upstream - this); - } - - @Override - protected MyUserIdProvider createMyUserIdProvider() { - if (sOverrides.mMyUserIdProvider != null) { - return sOverrides.mMyUserIdProvider; - } - return super.createMyUserIdProvider(); + this, + userHandle, + new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource)); } @Override @@ -94,8 +107,8 @@ public class ResolverWrapperActivity extends ResolverActivity { return super.createWorkProfileAvailabilityManager(); } - ResolverWrapperAdapter getAdapter() { - return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter(); + ResolverListAdapter getAdapter() { + return mMultiProfilePagerAdapter.getActiveListAdapter(); } ResolverListAdapter getPersonalListAdapter() { @@ -118,12 +131,13 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override - public void safelyStartActivity(TargetInfo cti) { - if (sOverrides.onSafelyStartCallback != null && - sOverrides.onSafelyStartCallback.apply(cti)) { + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) { return; } - super.safelyStartActivity(cti); + super.safelyStartActivityInternal(cti, user, options); } @Override @@ -152,10 +166,21 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override + protected UserHandle getCloneProfileUserHandle() { + return sOverrides.cloneProfileUserHandle; + } + + @Override public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { super.startActivityAsUser(intent, options, user); } + @Override + protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle + userHandle) { + return super.getResolverRankerServiceUserHandleListInternal(userHandle); + } + /** * We cannot directly mock the activity created since instrumentation creates it. * <p> @@ -164,25 +189,28 @@ public class ResolverWrapperActivity extends ResolverActivity { static class OverrideData { @SuppressWarnings("Since15") public Function<PackageManager, PackageManager> createPackageManager; - public Function<TargetInfo, Boolean> onSafelyStartCallback; + public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback; public ResolverListController resolverListController; public ResolverListController workResolverListController; public Boolean isVoiceInteraction; public UserHandle workProfileUserHandle; + public UserHandle cloneProfileUserHandle; + public UserHandle tabOwnerUserHandleForLaunch; public Integer myUserId; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public WorkProfileAvailabilityManager mWorkProfileAvailability; - public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public void reset() { - onSafelyStartCallback = null; + onSafelyStartInternalCallback = null; isVoiceInteraction = null; createPackageManager = null; resolverListController = mock(ResolverListController.class); workResolverListController = mock(ResolverListController.class); workProfileUserHandle = null; + cloneProfileUserHandle = null; + tabOwnerUserHandleForLaunch = null; myUserId = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; @@ -212,16 +240,55 @@ public class ResolverWrapperActivity extends ResolverActivity { } }; - mMyUserIdProvider = new MyUserIdProvider() { - @Override - public int getMyUserId() { - return myUserId != null ? myUserId : UserHandle.myUserId(); - } - }; - mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) .thenAnswer(invocation -> hasCrossProfileIntents); } } + + private static class TargetDataLoaderWrapper extends TargetDataLoader { + private final TargetDataLoader mTargetDataLoader; + private final CountingIdlingResource mLabelIdlingResource; + + private TargetDataLoaderWrapper( + TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) { + mTargetDataLoader = targetDataLoader; + mLabelIdlingResource = labelIdlingResource; + } + + @Override + public void loadAppTargetIcon( + @NonNull DisplayResolveInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); + } + + @Override + public void loadDirectShareIcon( + @NonNull SelectableTargetInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback); + } + + @Override + public void loadLabel( + @NonNull DisplayResolveInfo info, + @NonNull Consumer<CharSequence[]> callback) { + mLabelIdlingResource.increment(); + mTargetDataLoader.loadLabel( + info, + (result) -> { + mLabelIdlingResource.decrement(); + callback.accept(result); + }); + } + + @NonNull + @Override + public TargetPresentationGetter createPresentationGetter(@NonNull ResolveInfo info) { + return mTargetDataLoader.createPresentationGetter(info); + } + } } diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java deleted file mode 100644 index a53b41d1..00000000 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2019 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.Context; -import android.content.Intent; -import android.content.pm.ResolveInfo; -import android.os.UserHandle; - -import androidx.test.espresso.idling.CountingIdlingResource; - -import com.android.intentresolver.chooser.DisplayResolveInfo; - -import java.util.List; - -public class ResolverWrapperAdapter extends ResolverListAdapter { - - private CountingIdlingResource mLabelIdlingResource = - new CountingIdlingResource("LoadLabelTask"); - - public ResolverWrapperAdapter( - Context context, - List<Intent> payloadIntents, - Intent[] initialIntents, - List<ResolveInfo> rList, - boolean filterLastUsed, - ResolverListController resolverListController, - UserHandle userHandle, - Intent targetIntent, - ResolverListCommunicator resolverListCommunicator) { - super( - context, - payloadIntents, - initialIntents, - rList, - filterLastUsed, - resolverListController, - userHandle, - targetIntent, - resolverListCommunicator, - false); - } - - public CountingIdlingResource getLabelIdlingResource() { - return mLabelIdlingResource; - } - - @Override - protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { - return new LoadLabelWrapperTask(info); - } - - class LoadLabelWrapperTask extends LoadLabelTask { - - protected LoadLabelWrapperTask(DisplayResolveInfo dri) { - super(dri); - } - - @Override - protected void onPreExecute() { - mLabelIdlingResource.increment(); - } - - @Override - protected void onPostExecute(CharSequence[] result) { - super.onPostExecute(result); - mLabelIdlingResource.decrement(); - } - } -} diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt index a8d6f978..9ddeed84 100644 --- a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt +++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt @@ -21,10 +21,12 @@ import android.content.Context import android.content.Intent import android.content.pm.ResolveInfo import android.content.pm.ShortcutInfo +import android.os.UserHandle import android.service.chooser.ChooserTarget import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.chooser.TargetInfo import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -35,6 +37,9 @@ private const val CLASS_NAME = "./MainActivity" @SmallTest class ShortcutSelectionLogicTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply { arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg -> // shortcuts in reverse priority order @@ -52,7 +57,7 @@ class ShortcutSelectionLogicTest { private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(3, 0), + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), "label", "extended info", Intent(), @@ -60,7 +65,7 @@ class ShortcutSelectionLogicTest { private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(4, 0), + ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE), "label 2", "extended info 2", Intent(), diff --git a/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt new file mode 100644 index 00000000..d239f612 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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 androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import com.android.intentresolver.contentpreview.BasePreviewViewModel +import com.android.intentresolver.contentpreview.ImageLoader +import com.android.intentresolver.contentpreview.PreviewDataProvider + +/** A test content preview model that supports image loader override. */ +class TestContentPreviewViewModel( + private val viewModel: BasePreviewViewModel, + private val imageLoader: ImageLoader? = null, +) : BasePreviewViewModel() { + override fun createOrReuseProvider( + chooserRequest: ChooserRequestParameters + ): PreviewDataProvider = viewModel.createOrReuseProvider(chooserRequest) + + override fun createOrReuseImageLoader(): ImageLoader = + imageLoader ?: viewModel.createOrReuseImageLoader() + + companion object { + fun wrap( + factory: ViewModelProvider.Factory, + imageLoader: ImageLoader?, + ): ViewModelProvider.Factory = + object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun <T : ViewModel> create( + modelClass: Class<T>, + extras: CreationExtras + ): T { + return TestContentPreviewViewModel( + factory.create(modelClass, extras) as BasePreviewViewModel, + imageLoader, + ) as T + } + } + } +} diff --git a/java/tests/src/com/android/intentresolver/TestContentProvider.kt b/java/tests/src/com/android/intentresolver/TestContentProvider.kt new file mode 100644 index 00000000..b3b53baa --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestContentProvider.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2023 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.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +class TestContentProvider : ContentProvider() { + override fun query( + uri: Uri, + projection: Array<out String>?, + selection: String?, + selectionArgs: Array<out String>?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? + = runCatching { + uri.getQueryParameter("mimeType") + }.getOrNull() + + override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? + = runCatching { + uri.getQueryParameter("streamType")?.let { arrayOf(it) } + }.getOrNull() + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<out String>? + ): Int = 0 + + override fun onCreate(): Boolean = true +}
\ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt index cfe041dd..bf87ed8a 100644 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -18,21 +18,16 @@ package com.android.intentresolver import android.graphics.Bitmap import android.net.Uri +import androidx.lifecycle.Lifecycle +import com.android.intentresolver.contentpreview.ImageLoader import java.util.function.Consumer -internal class TestPreviewImageLoader( - private val imageLoader: ImageLoader, - private val imageOverride: () -> Bitmap? -) : ImageLoader { - override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) { - val override = imageOverride() - if (override != null) { - callback.accept(override) - } else { - imageLoader.loadImage(uri, callback) - } +internal class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader { + override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) { + callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(uri) + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + override fun prePopulate(uris: List<Uri>) = Unit } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 9ffd02d4..3ddd4394 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -26,6 +26,7 @@ import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; @@ -79,6 +80,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; @@ -90,19 +92,21 @@ import android.util.HashedStringCache; import android.util.Pair; import android.util.SparseArray; import android.view.View; -import android.view.ViewGroup; +import android.view.WindowManager; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; +import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.flags.Flags; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -125,6 +129,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -155,6 +160,8 @@ public class UnbundledChooserActivityTest { * -------- */ + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm; private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM = pm -> { @@ -164,11 +171,7 @@ public class UnbundledChooserActivityTest { }; private static final List<BooleanFlag> ALL_FLAGS = - Arrays.asList( - Flags.SHARESHEET_CUSTOM_ACTIONS, - Flags.SHARESHEET_RESELECTION_ACTION, - Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); + Arrays.asList(); private static final Map<BooleanFlag, Boolean> ALL_FLAGS_OFF = createAllFlagsOverride(false); @@ -177,11 +180,20 @@ public class UnbundledChooserActivityTest { @Parameterized.Parameters public static Collection packageManagers() { + if (ALL_FLAGS.isEmpty()) { + // No flags to toggle between, so just two configurations. + return Arrays.asList(new Object[][] { + // Default PackageManager and all flags off + { DEFAULT_PM, ALL_FLAGS_OFF}, + // No App Prediction Service and all flags off + { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, + }); + } return Arrays.asList(new Object[][] { // Default PackageManager and all flags off - { DEFAULT_PM, ALL_FLAGS_OFF }, + { DEFAULT_PM, ALL_FLAGS_OFF}, // Default PackageManager and all flags on - { DEFAULT_PM, ALL_FLAGS_ON }, + { DEFAULT_PM, ALL_FLAGS_ON}, // No App Prediction Service and all flags off { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, // No App Prediction Service and all flags on @@ -350,7 +362,7 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); waitForIdle(); onView(withId(android.R.id.title)) - .check(matches(withText(com.android.internal.R.string.whichSendApplication))); + .check(matches(withText(R.string.whichSendApplication))); } @Test @@ -362,7 +374,7 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(android.R.id.title)) - .check(matches(withText(com.android.internal.R.string.whichSendApplication))); + .check(matches(withText(R.string.whichSendApplication))); } @Test @@ -415,10 +427,12 @@ public class UnbundledChooserActivityTest { @Test public void visiblePreviewTitleAndThumbnail() throws InterruptedException { String previewTitle = "My Content Preview Title"; - Intent sendIntent = createSendTextIntentWithPreview(previewTitle, - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -445,7 +459,7 @@ public class UnbundledChooserActivityTest { onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -472,7 +486,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> infosToStack = new ArrayList<>(); for (int i = 0; i < 4; i++) { ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i, - UserHandle.USER_CURRENT); + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); resolveInfo.activityInfo.applicationInfo.name = appName; resolveInfo.activityInfo.applicationInfo.packageName = packageName; resolveInfo.activityInfo.packageName = packageName; @@ -491,7 +505,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(6)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -522,17 +536,21 @@ public class UnbundledChooserActivityTest { verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) .topK(any(List.class), anyInt()); assertThat(activity.getIsSelected(), is(false)); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { return true; }; ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo(sendIntent, toChoose, "testLabel", "testInfo", + sendIntent, /* resolveInfoPresentationGetter */ null); onView(withText(toChoose.activityInfo.name)) .perform(click()); waitForIdle(); verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) - .updateChooserCounts(Mockito.anyString(), anyInt(), Mockito.anyString()); + .updateChooserCounts(Mockito.anyString(), any(UserHandle.class), + Mockito.anyString()); verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) - .updateModel(toChoose.activityInfo.getComponentName()); + .updateModel(testDri); assertThat(activity.getIsSelected(), is(true)); } @@ -560,7 +578,7 @@ public class UnbundledChooserActivityTest { @Test public void autoLaunchSingleResult() throws InterruptedException { ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -595,7 +613,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -630,7 +648,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -661,7 +679,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -676,24 +694,21 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) - public void testImagePlusTextSharing_ExcludeText() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + @Ignore("b/285309527") + public void testFilePlusTextSharing_ExcludeText() { + Uri uri = createTestContentProviderUri(null, "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - new Intent("VIEW_TEXT")) + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -706,8 +721,10 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -719,25 +736,22 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) - public void testImagePlusTextSharing_RemoveAndAddBackText() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + @Ignore("b/285309527") + public void testFilePlusTextSharing_RemoveAndAddBackText() { + Uri uri = createTestContentProviderUri("application/pdf", "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); final String text = "https://google.com/search?q=google"; sendIntent.putExtra(Intent.EXTRA_TEXT, text); List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - new Intent("VIEW_TEXT")) + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -749,12 +763,16 @@ public class UnbundledChooserActivityTest { .check(matches(isDisplayed())) .perform(click()); waitForIdle(); + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + onView(withId(R.id.include_text_action)) .perform(click()); waitForIdle(); + onView(withId(R.id.content_preview_text)).check(matches(withText(text))); + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -766,15 +784,12 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) - public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + @Ignore("b/285309527") + public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); Intent alternativeIntent = createSendTextIntent(); @@ -784,10 +799,10 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - alternativeIntent) + alternativeIntent, PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -801,7 +816,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -813,6 +828,40 @@ public class UnbundledChooserActivityTest { } @Test + @Ignore("b/285309527") + public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.image_view)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + onView(withId(R.id.content_preview_text)) + .check(matches(allOf(isDisplayed(), withText("Image only")))); + } + + @Test public void copyTextToClipboard() throws Exception { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -823,8 +872,8 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); ClipboardManager clipboard = (ClipboardManager) activity.getSystemService( Context.CLIPBOARD_SERVICE); ClipData clipData = clipboard.getPrimaryClip(); @@ -847,8 +896,8 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); ChooserActivityLogger logger = activity.getChooserActivityLogger(); verify(logger, times(1)).logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_COPY)); @@ -876,13 +925,11 @@ public class UnbundledChooserActivityTest { @Test @Ignore - public void testEditImageLogs() throws Exception { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + public void testEditImageLogs() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -901,113 +948,63 @@ public class UnbundledChooserActivityTest { @Test public void oneVisibleImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); + Uri uri = createTestContentProviderUri("image/png", null); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createWideBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_area)) + onView(withId(R.id.scrollable_image_preview)) .check((view, exception) -> { if (exception != null) { throw exception; } - ViewGroup parent = (ViewGroup) view; - ArrayList<View> visibleViews = new ArrayList<>(); - for (int i = 0, count = parent.getChildCount(); i < count; i++) { - View child = parent.getChildAt(i); - if (child.getVisibility() == View.VISIBLE) { - visibleViews.add(child); - } - } - assertThat(visibleViews.size(), is(1)); + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(1)); + assertThat(recyclerView.getChildCount(), is(1)); + View imageView = recyclerView.getChildAt(0); + Rect rect = new Rect(); + boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect); assertThat( - "image preview view is fully visible", - isDisplayed().matches(visibleViews.get(0))); + "image preview view is not fully visible", + isPartiallyVisible + && rect.width() == imageView.getWidth() + && rect.height() == imageView.getHeight()); }); } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { false }) - public void twoVisibleImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); + public void allThumbnailsFailedToLoad_hidePreview() { + Uri uri = createTestContentProviderUri("image/jpg", null); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_1_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_small)) - .check(matches(not(isDisplayed()))); - onView(withId(com.android.internal.R.id.content_preview_image_3_small)) - .check(matches(not(isDisplayed()))); - } - - @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { false }) - public void threeOrMoreVisibleImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); - - ArrayList<Uri> uris = new ArrayList<>(); - uris.add(uri); - uris.add(uri); - uris.add(uri); - uris.add(uri); - uris.add(uri); - - Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; - - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - - setupResolverControllers(resolvedComponentInfos); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_1_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_large)) - .check(matches(not(isDisplayed()))); - onView(withId(com.android.internal.R.id.content_preview_image_2_small)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_3_small)) - .check(matches(isDisplayed())); + onView(withId(R.id.scrollable_image_preview)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { true }) public void testManyVisibleImagePreview_ScrollableImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); + Uri uri = createTestContentProviderUri("image/png", null); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); @@ -1022,15 +1019,15 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_area)) + onView(withId(R.id.scrollable_image_preview)) .perform(RecyclerViewActions.scrollToLastPosition()) .check((view, exception) -> { if (exception != null) { @@ -1042,12 +1039,8 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testImageAndTextPreview() { - final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); + final Uri uri = createTestContentProviderUri("image/png", null); final String sharedText = "text-" + System.currentTimeMillis(); ArrayList<Uri> uris = new ArrayList<>(); @@ -1055,8 +1048,8 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1068,6 +1061,38 @@ public class UnbundledChooserActivityTest { } @Test + public void testTextPreviewWhenTextIsSharedWithMultipleImages() { + final Uri uri = createTestContentProviderUri("image/png", null); + final String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + Mockito.any(UserHandle.class))) + .thenReturn(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText)).check(matches(isDisplayed())); + } + + @Test public void testOnCreateLogging() { Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); @@ -1127,15 +1152,14 @@ public class UnbundledChooserActivityTest { @Test public void testImagePreviewLogging() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); + Uri uri = createTestContentProviderUri("image/png", null); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1162,12 +1186,9 @@ public class UnbundledChooserActivityTest { setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(withText("app.pdf"))); - onView(withId(com.android.internal.R.id.content_preview_file_icon)) - .check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); } @@ -1187,12 +1208,11 @@ public class UnbundledChooserActivityTest { setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(withText("app.pdf + 2 files"))); - onView(withId(com.android.internal.R.id.content_preview_file_icon)) - .check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 2 more files"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); } @Test @@ -1211,12 +1231,9 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(withText("app.pdf"))); - onView(withId(com.android.internal.R.id.content_preview_file_icon)) - .check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); } @Test @@ -1242,12 +1259,11 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_filename)) - .check(matches(withText("app.pdf + 1 file"))); - onView(withId(com.android.internal.R.id.content_preview_file_icon)) - .check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 1 more file"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); } @Test @@ -1271,8 +1287,12 @@ public class UnbundledChooserActivityTest { waitForIdle(); final DisplayResolveInfo testDri = - activity.createTestDisplayResolveInfo(sendIntent, - ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent, + activity.createTestDisplayResolveInfo( + sendIntent, + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), + "testLabel", + "testInfo", + sendIntent, /* resolveInfoPresentationGetter */ null); final ChooserListAdapter adapter = activity.getAdapter(); @@ -1305,7 +1325,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1386,7 +1406,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1471,7 +1491,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1546,7 +1566,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1611,7 +1631,8 @@ public class UnbundledChooserActivityTest { // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - setupResolverControllers(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos); + markWorkProfileUserAvailable(); // set caller-provided target Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); @@ -1638,7 +1659,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1667,12 +1688,20 @@ public class UnbundledChooserActivityTest { "The display label must match", activeAdapter.getItem(0).getDisplayLabel(), is(callerTargetLabel)); + + // Switch to work profile and ensure that the target *doesn't* show up there. + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + for (int i = 0; i < activity.getWorkListAdapter().getCount(); i++) { + assertThat( + "Chooser target should not show up in opposite profile", + activity.getWorkListAdapter().getItem(i).getDisplayLabel(), + not(callerTargetLabel)); + } } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_CUSTOM_ACTIONS_NAME }, - values = { true }) public void testLaunchWithCustomAction() throws InterruptedException { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -1716,9 +1745,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_RESELECTION_ACTION_NAME }, - values = { true }) public void testLaunchWithShareModification() throws InterruptedException { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -1726,13 +1752,17 @@ public class UnbundledChooserActivityTest { Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); final String modifyShareAction = "test-broadcast-receiver-action"; Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + String label = "modify share"; + PendingIntent pendingIntent = PendingIntent.getBroadcast( + testContext, + 123, + new Intent(modifyShareAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap( + createBitmap()), label, pendingIntent).build(); chooserIntent.putExtra( Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, - PendingIntent.getBroadcast( - testContext, - 123, - new Intent(modifyShareAction), - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)); + action); // Start activity mActivityRule.launchActivity(chooserIntent); waitForIdle(); @@ -1747,7 +1777,7 @@ public class UnbundledChooserActivityTest { testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction)); try { - onView(withText(R.string.select_text)).perform(click()); + onView(withText(label)).perform(click()); broadcastInvoked.await(); } finally { testContext.unregisterReceiver(testReceiver); @@ -1810,7 +1840,7 @@ public class UnbundledChooserActivityTest { // Create direct share target List<ChooserTarget> serviceTargets = createDirectShareTargets(1, resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE); // Start activity final IChooserWrapper wrapper = (IChooserWrapper) @@ -1943,7 +1973,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2099,7 +2129,7 @@ public class UnbundledChooserActivityTest { onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2139,7 +2169,7 @@ public class UnbundledChooserActivityTest { ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); verify(shortcutLoaders.get(0).first, times(1)) - .queryShortcuts(appTargets.capture()); + .updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -2220,7 +2250,7 @@ public class UnbundledChooserActivityTest { ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); verify(shortcutLoaders.get(0).first, times(1)) - .queryShortcuts(appTargets.capture()); + .updateAppTargets(appTargets.capture()); // send shortcuts List<ChooserTarget> serviceTargets = createDirectShareTargets( @@ -2315,7 +2345,7 @@ public class UnbundledChooserActivityTest { } @Test - public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() { + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2326,7 +2356,7 @@ public class UnbundledChooserActivityTest { setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendTextIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2334,7 +2364,7 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); waitForIdle(); - assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); + assertNull(chosen[0]); } @Test @@ -2345,7 +2375,7 @@ public class UnbundledChooserActivityTest { Intent chooserIntent = createChooserIntent(createSendTextIntent(), new Intent[] {new Intent("action.fake")}); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2473,7 +2503,7 @@ public class UnbundledChooserActivityTest { new Intent[] {new Intent("action.fake")}); ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); ResolveInfo ri = ResolverDataProvider.createResolveInfo(0, - UserHandle.USER_CURRENT); + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); when( ChooserActivityOverrideData .getInstance() @@ -2515,12 +2545,52 @@ public class UnbundledChooserActivityTest { .perform(swipeUp()); waitForIdle(); - verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any()); + verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - verify(workProfileShortcutLoader, times(1)).queryShortcuts(any()); + verify(workProfileShortcutLoader, times(1)).updateAppTargets(any()); + } + + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markCloneProfileUserAvailable(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + ChooserActivityOverrideData.getInstance().cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + + final IChooserWrapper activity = (IChooserWrapper) mActivityRule + .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest")); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markWorkProfileUserAvailable(); + markCloneProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + 4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); } private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { @@ -2557,6 +2627,7 @@ public class UnbundledChooserActivityTest { ri.activityInfo.packageName = "fake.package.name"; ri.activityInfo.applicationInfo = new ApplicationInfo(); ri.activityInfo.applicationInfo.packageName = "fake.package.name"; + ri.userHandle = UserHandle.CURRENT; return ri; } @@ -2581,6 +2652,21 @@ public class UnbundledChooserActivityTest { return sendIntent; } + private Uri createTestContentProviderUri( + @Nullable String mimeType, @Nullable String streamType) { + String packageName = + InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); + Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png") + .buildUpon(); + if (mimeType != null) { + builder.appendQueryParameter("mimeType", mimeType); + } + if (streamType != null) { + builder.appendQueryParameter("streamType", streamType); + } + return builder.build(); + } + private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -2618,7 +2704,23 @@ public class UnbundledChooserActivityTest { private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); } return infoList; } @@ -2628,9 +2730,11 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + PERSONAL_USER_HANDLE)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); } } return infoList; @@ -2642,9 +2746,11 @@ public class UnbundledChooserActivityTest { for (int i = 0; i < numberOfResults; i++) { if (i == 0) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); } } return infoList; @@ -2654,7 +2760,8 @@ public class UnbundledChooserActivityTest { int numberOfResults, int userId) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); } return infoList; } @@ -2686,8 +2793,22 @@ public class UnbundledChooserActivityTest { } private Bitmap createBitmap() { - int width = 200; - int height = 200; + return createBitmap(200, 200); + } + + private Bitmap createWideBitmap() { + WindowManager windowManager = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getSystemService(WindowManager.class); + int width = 3000; + if (windowManager != null) { + Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); + width = bounds.width() + 200; + } + return createBitmap(width, 100); + } + + private Bitmap createBitmap(int width, int height) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); @@ -2733,6 +2854,10 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); } + private void markCloneProfileUserAvailable() { + ChooserActivityOverrideData.getInstance().cloneProfileUserHandle = UserHandle.of(11); + } + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos) { setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); @@ -2839,4 +2964,8 @@ public class UnbundledChooserActivityTest { }; return shortcutLoaders; } + + private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { + return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); + } } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java index 87dc1b9d..92bccb7d 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -49,7 +49,6 @@ import androidx.test.espresso.NoMatchingViewException; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; -import com.android.internal.R; import junit.framework.AssertionFailedError; @@ -99,7 +98,7 @@ public class UnbundledChooserActivityWorkProfileTest { public void testBlocker() { setUpPersonalAndWorkComponentInfos(); sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); - sOverrides.myUserId = mTestCase.getMyUserHandle().getIdentifier(); + sOverrides.tabOwnerUserHandleForLaunch = mTestCase.getMyUserHandle(); launchActivity(mTestCase.getIsSendAction()); switchToTab(mTestCase.getTab()); @@ -242,19 +241,21 @@ public class UnbundledChooserActivityWorkProfileTest { } private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( - int numberOfResults, int userId) { + int numberOfResults, int userId, UserHandle resolvedForUser) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider + .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser)); } return infoList; } - private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } return infoList; } @@ -264,9 +265,9 @@ public class UnbundledChooserActivityWorkProfileTest { int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3, - /* userId */ WORK_USER_HANDLE.getIdentifier()); + /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE); List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); } @@ -356,7 +357,7 @@ public class UnbundledChooserActivityWorkProfileTest { } }); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); waitForIdle(); } diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt index dff1e5ae..504cfd97 100644 --- a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt +++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt @@ -30,14 +30,18 @@ import com.android.intentresolver.ResolverActivity import com.android.intentresolver.ResolverDataProvider import com.google.common.truth.Truth.assertThat import org.junit.Test +import androidx.test.platform.app.InstrumentationRegistry class ImmutableTargetInfoTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val resolvedIntent = Intent("resolved") private val targetIntent = Intent("target") private val referrerFillInIntent = Intent("referrer_fillin") private val resolvedComponentName = ComponentName("resolved", "component") private val chooserTargetComponentName = ComponentName("chooser", "target") - private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0) + private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE) private val displayLabel: CharSequence = "Display Label" private val extendedInfo: CharSequence = "Extended Info" private val displayIconHolder: TargetInfo.IconHolder = mock() @@ -45,14 +49,14 @@ class ImmutableTargetInfoTest { private val sourceIntent2 = Intent("source2") private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo( Intent("display1"), - ResolverDataProvider.createResolveInfo(2, 0), + ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE), "display1 label", "display1 extended info", Intent("display1_resolved"), /* resolveInfoPresentationGetter= */ null) private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo( Intent("display2"), - ResolverDataProvider.createResolveInfo(3, 0), + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), "display2 label", "display2 extended info", Intent("display2_resolved"), @@ -66,7 +70,7 @@ class ImmutableTargetInfoTest { UserHandle.CURRENT) private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent("displayresolve"), - ResolverDataProvider.createResolveInfo(5, 0), + ResolverDataProvider.createResolveInfo(5, 0, PERSONAL_USER_HANDLE), "displayresolve label", "displayresolve extended info", Intent("display_resolved"), diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt index 3d832cc9..f9d3dd96 100644 --- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt +++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt @@ -41,6 +41,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify class TargetInfoTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val context = InstrumentationRegistry.getInstrumentation().getContext() @Before @@ -81,7 +84,7 @@ class TargetInfoTest { val resolvedIntent = Intent() val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( resolvedIntent, - ResolverDataProvider.createResolveInfo(1, 0), + ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE), "label", "extended info", resolvedIntent, @@ -190,7 +193,7 @@ class TargetInfoTest { intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") intent.setType("text/plain") - val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) val targetInfo = DisplayResolveInfo.newDisplayResolveInfo( intent, @@ -268,7 +271,7 @@ class TargetInfoTest { intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") intent.setType("text/plain") - val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( intent, resolveInfo, diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index d870a8c2..9bfd2052 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -16,188 +16,131 @@ package com.android.intentresolver.contentpreview -import android.content.ClipDescription -import android.content.ContentInterface import android.content.Intent import android.graphics.Bitmap import android.net.Uri -import com.android.intentresolver.ImageLoader -import com.android.intentresolver.TestFeatureFlagRepository +import androidx.lifecycle.Lifecycle +import com.android.intentresolver.any import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory -import com.android.intentresolver.flags.Flags import com.android.intentresolver.mock import com.android.intentresolver.whenever import com.android.intentresolver.widget.ActionRow import com.android.intentresolver.widget.ImagePreviewView import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer import org.junit.Test import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.util.function.Consumer -private const val PROVIDER_NAME = "org.pkg.app" class ChooserContentPreviewUiTest { - private val contentResolver = mock<ContentInterface>() - private val imageClassifier = ChooserContentPreviewUi.ImageMimeTypeClassifier { mimeType -> - mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*") - } - private val imageLoader = object : ImageLoader { - override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) { - callback.accept(null) + private val lifecycle = mock<Lifecycle>() + private val previewData = mock<PreviewDataProvider>() + private val headlineGenerator = mock<HeadlineGenerator>() + private val imageLoader = + object : ImageLoader { + override fun loadImage( + callerLifecycle: Lifecycle, + uri: Uri, + callback: Consumer<Bitmap?>, + ) { + callback.accept(null) + } + override fun prePopulate(uris: List<Uri>) = Unit + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = null + } + private val actionFactory = + object : ActionFactory { + override fun getCopyButtonRunnable(): Runnable? = null + override fun getEditButtonRunnable(): Runnable? = null + override fun createCustomActions(): List<ActionRow.Action> = emptyList() + override fun getModifyShareAction(): ActionRow.Action? = null + override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} } - override fun prePopulate(uris: List<Uri>) = Unit - override suspend fun invoke(uri: Uri): Bitmap? = null - } - private val actionFactory = object : ActionFactory { - override fun createCopyButton() = ActionRow.Action(label = "Copy", icon = null) {} - override fun createEditButton(): ActionRow.Action? = null - override fun createNearbyButton(): ActionRow.Action? = null - override fun createCustomActions(): List<ActionRow.Action> = emptyList() - override fun getModifyShareAction(): Runnable? = null - override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} - } private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>() - private val featureFlagRepository = TestFeatureFlagRepository( - mapOf( - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW to true - ) - ) @Test - fun test_ChooserContentPreview_non_send_intent_action_to_text_preview() { - val targetIntent = Intent(Intent.ACTION_VIEW) - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) + fun test_textPreviewType_useTextPreviewUi() { + whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT) + val testSubject = + ChooserContentPreviewUi( + lifecycle, + previewData, + Intent(Intent.ACTION_VIEW), + imageLoader, + actionFactory, + transitionCallback, + headlineGenerator, + ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + assertThat(testSubject.mContentPreviewUi).isInstanceOf(TextContentPreviewUi::class.java) verify(transitionCallback, times(1)).onAllTransitionElementsReady() } @Test - fun test_ChooserContentPreview_text_mime_type_to_text_preview() { - val targetIntent = Intent(Intent.ACTION_SEND).apply { - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "Text Extra") - } - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) + fun test_filePreviewType_useFilePreviewUi() { + whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE) + val testSubject = + ChooserContentPreviewUi( + lifecycle, + previewData, + Intent(Intent.ACTION_SEND), + imageLoader, + actionFactory, + transitionCallback, + headlineGenerator, + ) assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.mContentPreviewUi).isInstanceOf(FileContentPreviewUi::class.java) verify(transitionCallback, times(1)).onAllTransitionElementsReady() } @Test - fun test_ChooserContentPreview_single_image_uri_to_image_preview() { - val uri = Uri.parse("content://$PROVIDER_NAME/test.png") - val targetIntent = Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, uri) - } - whenever(contentResolver.getType(uri)).thenReturn("image/png") - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) - assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) - verify(transitionCallback, never()).onAllTransitionElementsReady() - } - - @Test - fun test_ChooserContentPreview_single_non_image_uri_to_file_preview() { - val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") - val targetIntent = Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, uri) - } - whenever(contentResolver.getType(uri)).thenReturn("application/pdf") - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) - assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + fun test_imagePreviewTypeWithText_useFilePlusTextPreviewUi() { + val uri = Uri.parse("content://org.pkg.app/img.png") + whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + whenever(previewData.uriCount).thenReturn(2) + whenever(previewData.firstFileInfo) + .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) + val testSubject = + ChooserContentPreviewUi( + lifecycle, + previewData, + Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") }, + imageLoader, + actionFactory, + transitionCallback, + headlineGenerator, + ) + assertThat(testSubject.mContentPreviewUi) + .isInstanceOf(FilesPlusTextContentPreviewUi::class.java) + verify(previewData, times(1)).getFileMetadataForImagePreview(any(), any()) verify(transitionCallback, times(1)).onAllTransitionElementsReady() } @Test - fun test_ChooserContentPreview_multiple_image_uri_to_image_preview() { - val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png") - val uri2 = Uri.parse("content://$PROVIDER_NAME/test.jpg") - val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { - putExtra( - Intent.EXTRA_STREAM, - ArrayList<Uri>().apply { - add(uri1) - add(uri2) - } + fun test_imagePreviewTypeWithoutText_useImagePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/img.png") + whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + whenever(previewData.uriCount).thenReturn(2) + whenever(previewData.firstFileInfo) + .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) + val testSubject = + ChooserContentPreviewUi( + lifecycle, + previewData, + Intent(Intent.ACTION_SEND), + imageLoader, + actionFactory, + transitionCallback, + headlineGenerator, ) - } - whenever(contentResolver.getType(uri1)).thenReturn("image/png") - whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg") - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java) + verify(previewData, times(1)).getFileMetadataForImagePreview(any(), any()) verify(transitionCallback, never()).onAllTransitionElementsReady() } - - @Test - fun test_ChooserContentPreview_some_non_image_uri_to_file_preview() { - val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png") - val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf") - val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { - putExtra( - Intent.EXTRA_STREAM, - ArrayList<Uri>().apply { - add(uri1) - add(uri2) - } - ) - } - whenever(contentResolver.getType(uri1)).thenReturn("image/png") - whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") - val testSubject = ChooserContentPreviewUi( - targetIntent, - contentResolver, - imageClassifier, - imageLoader, - actionFactory, - transitionCallback, - featureFlagRepository - ) - assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - verify(transitionCallback, times(1)).onAllTransitionElementsReady() - } } diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt new file mode 100644 index 00000000..6db53a9e --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 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.contentpreview + +import com.android.intentresolver.widget.ScrollableImagePreviewView.PreviewType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ContentPreviewUiTest { + @Test + fun testPreviewTypes() { + val typeClassifier = + object : MimeTypeClassifier { + override fun isImageType(type: String?) = (type == "image") + override fun isVideoType(type: String?) = (type == "video") + } + + assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "image")) + .isEqualTo(PreviewType.Image) + assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "video")) + .isEqualTo(PreviewType.Video) + assertThat(ContentPreviewUi.getPreviewType(typeClassifier, "other")) + .isEqualTo(PreviewType.File) + assertThat(ContentPreviewUi.getPreviewType(typeClassifier, null)) + .isEqualTo(PreviewType.File) + } +} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt new file mode 100644 index 00000000..a65280e5 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 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.contentpreview + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Test +import org.junit.runner.RunWith +import com.google.common.truth.Truth.assertThat + +@RunWith(AndroidJUnit4::class) +class HeadlineGeneratorImplTest { + @Test + fun testHeadlineGeneration() { + val generator = HeadlineGeneratorImpl( + InstrumentationRegistry.getInstrumentation().getTargetContext()) + val str = "Some string" + val url = "http://www.google.com" + + assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") + assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") + + assertThat(generator.getImagesWithTextHeadline(str, 1)).isEqualTo("Sharing image with text") + assertThat(generator.getImagesWithTextHeadline(url, 1)).isEqualTo("Sharing image with link") + assertThat(generator.getImagesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 images with text") + assertThat(generator.getImagesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 images with link") + + assertThat(generator.getVideosWithTextHeadline(str, 1)).isEqualTo("Sharing video with text") + assertThat(generator.getVideosWithTextHeadline(url, 1)).isEqualTo("Sharing video with link") + assertThat(generator.getVideosWithTextHeadline(str, 5)).isEqualTo("Sharing 5 videos with text") + assertThat(generator.getVideosWithTextHeadline(url, 5)).isEqualTo("Sharing 5 videos with link") + + assertThat(generator.getFilesWithTextHeadline(str, 1)).isEqualTo("Sharing file with text") + assertThat(generator.getFilesWithTextHeadline(url, 1)).isEqualTo("Sharing file with link") + assertThat(generator.getFilesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 files with text") + assertThat(generator.getFilesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 files with link") + + assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") + assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") + + assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video") + assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos") + + assertThat(generator.getFilesHeadline(1)).isEqualTo("Sharing 1 file") + assertThat(generator.getFilesHeadline(4)).isEqualTo("Sharing 4 files") + } +} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt new file mode 100644 index 00000000..6e57c289 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2023 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.contentpreview + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.net.Uri +import android.util.Size +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import com.android.intentresolver.TestLifecycleOwner +import com.android.intentresolver.any +import com.android.intentresolver.anyOrNull +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import com.google.common.truth.Truth.assertThat +import java.util.ArrayDeque +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.coroutines.yield +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class ImagePreviewImageLoaderTest { + private val imageSize = Size(300, 300) + private val uriOne = Uri.parse("content://org.package.app/image-1.png") + private val uriTwo = Uri.parse("content://org.package.app/image-2.png") + private val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val contentResolver = + mock<ContentResolver> { + whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap) + } + private val lifecycleOwner = TestLifecycleOwner() + private val dispatcher = UnconfinedTestDispatcher() + private lateinit var testSubject: ImagePreviewImageLoader + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + // create test subject after we've updated the lifecycle dispatcher + testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } + + @Test + fun prePopulate_cachesImagesUpToTheCacheSize() = runTest { + testSubject.prePopulate(listOf(uriOne, uriTwo)) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) + + testSubject(uriOne) + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } + + @Test + fun invoke_returnCachedImageWhenCalledTwice() = runTest { + testSubject(uriOne) + testSubject(uriOne) + + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_whenInstructed_doesNotCache() = runTest { + testSubject(uriOne, false) + testSubject(uriOne, false) + + verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_overlappedRequests_Deduplicate() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + scheduler.advanceUntilIdle() + } + + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_oldRecordsEvictedFromTheCache() = runTest { + testSubject(uriOne) + testSubject(uriTwo) + testSubject(uriTwo) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) + } + + @Test + fun invoke_doNotCacheNulls() = runTest { + whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) + testSubject(uriOne) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + } + + @Test(expected = CancellationException::class) + fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest { + lifecycleOwner.state = Lifecycle.State.DESTROYED + testSubject(uriOne) + } + + @Test(expected = CancellationException::class) + fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } + lifecycleOwner.state = Lifecycle.State.DESTROYED + scheduler.advanceUntilIdle() + deferred.await() + } + } + + @Test + fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, true) } + scheduler.advanceUntilIdle() + } + testSubject(uriOne, true) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } + + @Test + fun invoke_semaphoreGuardsContentResolverCalls() = runTest { + val contentResolver = + mock<ContentResolver> { + whenever(loadThumbnail(any(), any(), anyOrNull())) + .thenThrow(SecurityException("test")) + } + val acquireCount = AtomicInteger() + val releaseCount = AtomicInteger() + val testSemaphore = + object : Semaphore { + override val availablePermits: Int + get() = error("Unexpected invocation") + + override suspend fun acquire() { + acquireCount.getAndIncrement() + } + + override fun tryAcquire(): Boolean { + error("Unexpected invocation") + } + + override fun release() { + releaseCount.getAndIncrement() + } + } + + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + testSemaphore, + ) + testSubject(uriOne, false) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + assertThat(acquireCount.get()).isEqualTo(1) + assertThat(releaseCount.get()).isEqualTo(1) + } + + @Test + fun invoke_semaphoreIsReleasedAfterContentResolverFailure() = runTest { + val semaphoreDeferred = CompletableDeferred<Unit>() + val releaseCount = AtomicInteger() + val testSemaphore = + object : Semaphore { + override val availablePermits: Int + get() = error("Unexpected invocation") + + override suspend fun acquire() { + semaphoreDeferred.await() + } + + override fun tryAcquire(): Boolean { + error("Unexpected invocation") + } + + override fun release() { + releaseCount.getAndIncrement() + } + } + + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + testSemaphore, + ) + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + + verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) + + semaphoreDeferred.complete(Unit) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + assertThat(releaseCount.get()).isEqualTo(1) + } + + @Test + fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() { + val requestCount = 4 + val thumbnailCallsCdl = CountDownLatch(requestCount) + val pendingThumbnailCalls = ArrayDeque<CountDownLatch>() + val contentResolver = + mock<ContentResolver> { + whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer { + val latch = CountDownLatch(1) + synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) } + thumbnailCallsCdl.countDown() + latch.await() + bitmap + } + } + val name = "LoadImage" + val maxSimultaneousRequests = 2 + val threadsStartedCdl = CountDownLatch(requestCount) + val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() } + val testSubject = + ImagePreviewImageLoader( + lifecycleOwner.lifecycle.coroutineScope + dispatcher + CoroutineName(name), + imageSize.width, + contentResolver, + cacheSize = 1, + maxSimultaneousRequests, + ) + runTest { + repeat(requestCount) { + launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } + } + yield() + // wait for all requests to be dispatched + assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue() + + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } + + pendingThumbnailCalls.poll()?.countDown() + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } + + pendingThumbnailCalls.poll()?.countDown() + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } + for (cdl in pendingThumbnailCalls) { + cdl.countDown() + } + } + } +} + +private class NewThreadDispatcher( + private val coroutineName: String, + private val launchedCallback: () -> Unit +) : CoroutineDispatcher() { + override fun isDispatchNeeded(context: CoroutineContext): Boolean = true + + override fun dispatch(context: CoroutineContext, block: Runnable) { + Thread { + if (coroutineName == context[CoroutineName.Key]?.name) { + launchedCallback() + } + block.run() + } + .start() + } +} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt new file mode 100644 index 00000000..145b89ad --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2023 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.contentpreview + +import android.content.ContentInterface +import android.content.Intent +import android.database.MatrixCursor +import android.media.MediaMetadata +import android.net.Uri +import android.provider.DocumentsContract +import androidx.lifecycle.Lifecycle +import com.android.intentresolver.TestLifecycleOwner +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.any +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class PreviewDataProviderTest { + private val contentResolver = mock<ContentInterface>() + private val mimeTypeClassifier = DefaultMimeTypeClassifier + + private val lifecycleOwner = TestLifecycleOwner() + private val dispatcher = UnconfinedTestDispatcher() + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } + + @Test + fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() { + val targetIntent = Intent(Intent.ACTION_VIEW) + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + verify(contentResolver, never()).getType(any()) + } + + @Test + fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/notes.txt") + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/plain" + } + whenever(contentResolver.getType(uri)).thenReturn("text/plain") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() { + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + type = "image/png" + } + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + verify(contentResolver, never()).getType(any()) + } + + @Test + fun test_sendSingleImage_resolvesToImagePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/image.png") + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("image/png") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleNonImage_resolvesToFilePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/paper.pdf") + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/image.png") + val targetIntent = + Intent(Intent.ACTION_SEND) + .apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/image.png") + val targetIntent = + Intent(Intent.ACTION_SEND) + .apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) + .thenThrow(SecurityException("test failure")) + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { + val uri = Uri.parse("content://org.pkg.app/paper.pdf") + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenReturn(arrayOf("application/pdf", "image/png")) + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_SingleNonImageUriWithThumbnailFlag_useImagePreviewUi() { + testMetadataToImagePreview( + columns = arrayOf(DocumentsContract.Document.COLUMN_FLAGS), + values = + arrayOf( + DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL or + DocumentsContract.Document.FLAG_SUPPORTS_METADATA + ) + ) + } + + @Test + fun test_SingleNonImageUriWithMetadataIconUri_useImagePreviewUi() { + testMetadataToImagePreview( + columns = arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI), + values = arrayOf("content://org.pkg.app/test.pdf?thumbnail"), + ) + } + + private fun testMetadataToImagePreview(columns: Array<String>, values: Array<Any>) { + val uri = Uri.parse("content://org.pkg.app/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND) + .apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) + .thenReturn(MatrixCursor(columns).apply { addRow(values) }) + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNotNull() + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_multipleImageUri_useImagePreviewUi() { + val uri1 = Uri.parse("content://org.pkg.app/test.png") + val uri2 = Uri.parse("content://org.pkg.app/test.jpg") + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE) + .apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList<Uri>().apply { + add(uri1) + add(uri2) + } + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("image/png") + whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) + // preview type can be determined by the first URI type + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_SomeImageUri_useImagePreviewUi() { + val uri1 = Uri.parse("content://org.pkg.app/test.png") + val uri2 = Uri.parse("content://org.pkg.app/test.pdf") + whenever(contentResolver.getType(uri1)).thenReturn("image/png") + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE) + .apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList<Uri>().apply { + add(uri1) + add(uri2) + } + ) + } + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) + // preview type can be determined by the first URI type + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_someNonImageUriWithPreview_useImagePreviewUi() { + val uri1 = Uri.parse("content://org.pkg.app/test.mp4") + val uri2 = Uri.parse("content://org.pkg.app/test.pdf") + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE) + .apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList<Uri>().apply { + add(uri1) + add(uri2) + } + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4") + whenever(contentResolver.getStreamTypes(uri1, "*/*")).thenReturn(arrayOf("image/png")) + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri1) + verify(contentResolver, times(2)).getType(any()) + } + + @Test + fun test_allNonImageUrisWithoutPreview_useFilePreviewUi() { + val uri1 = Uri.parse("content://org.pkg.app/test.html") + val uri2 = Uri.parse("content://org.pkg.app/test.pdf") + val targetIntent = + Intent(Intent.ACTION_SEND_MULTIPLE) + .apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList<Uri>().apply { + add(uri1) + add(uri2) + } + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("text/html") + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val testSubject = + PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + verify(contentResolver, times(2)).getType(any()) + } +} diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java index 006f3b2d..5f0ead7b 100644 --- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java +++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java @@ -28,6 +28,9 @@ import android.os.Message; import androidx.test.InstrumentationRegistry; import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.chooser.TargetInfo; + +import com.google.android.collect.Lists; import org.junit.Test; @@ -37,51 +40,82 @@ public class AbstractResolverComparatorTest { @Test public void testPinned() { - ResolvedComponentInfo r1 = new ResolvedComponentInfo( - new ComponentName("package", "class"), new Intent(), new ResolveInfo() - ); + ResolvedComponentInfo r1 = createResolvedComponentInfo( + new ComponentName("package", "class")); r1.setPinned(true); - ResolvedComponentInfo r2 = new ResolvedComponentInfo( - new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo() - ); + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("zackage", "zlass")); Context context = InstrumentationRegistry.getTargetContext(); - AbstractResolverComparator comparator = getTestComparator(context); + AbstractResolverComparator comparator = getTestComparator(context, null); assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2)); assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1)); } - @Test public void testBothPinned() { - ResolveInfo pmInfo1 = new ResolveInfo(); - pmInfo1.activityInfo = new ActivityInfo(); - pmInfo1.activityInfo.packageName = "aaa"; - - ResolvedComponentInfo r1 = new ResolvedComponentInfo( - new ComponentName("package", "class"), new Intent(), pmInfo1); + ResolvedComponentInfo r1 = createResolvedComponentInfo( + new ComponentName("package", "class")); r1.setPinned(true); - ResolveInfo pmInfo2 = new ResolveInfo(); - pmInfo2.activityInfo = new ActivityInfo(); - pmInfo2.activityInfo.packageName = "zzz"; - ResolvedComponentInfo r2 = new ResolvedComponentInfo( - new ComponentName("zackage", "zlass"), new Intent(), pmInfo2); + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("zackage", "zlass")); r2.setPinned(true); Context context = InstrumentationRegistry.getTargetContext(); - AbstractResolverComparator comparator = getTestComparator(context); + AbstractResolverComparator comparator = getTestComparator(context, null); assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2)); } - private AbstractResolverComparator getTestComparator(Context context) { + @Test + public void testPromoteToFirst() { + ComponentName promoteToFirst = new ComponentName("promoted-package", "class"); + ResolvedComponentInfo r1 = createResolvedComponentInfo(promoteToFirst); + + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("package", "class")); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context, promoteToFirst); + + assertEquals("PromoteToFirst ranks over non-cemented", -1, comparator.compare(r1, r2)); + assertEquals("Non-cemented ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); + } + + @Test + public void testPromoteToFirstOverPinned() { + ComponentName cementedComponent = new ComponentName("promoted-package", "class"); + ResolvedComponentInfo r1 = createResolvedComponentInfo(cementedComponent); + + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("package", "class")); + r2.setPinned(true); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context, cementedComponent); + + assertEquals("PromoteToFirst ranks over pinned", -1, comparator.compare(r1, r2)); + assertEquals("Pinned ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); + } + + private ResolvedComponentInfo createResolvedComponentInfo(ComponentName component) { + ResolveInfo info = new ResolveInfo(); + info.activityInfo = new ActivityInfo(); + info.activityInfo.packageName = component.getPackageName(); + info.activityInfo.name = component.getClassName(); + return new ResolvedComponentInfo(component, new Intent(), info); + } + + private AbstractResolverComparator getTestComparator( + Context context, ComponentName promoteToFirst) { Intent intent = new Intent(); AbstractResolverComparator testComparator = - new AbstractResolverComparator(context, intent) { + new AbstractResolverComparator(context, intent, + Lists.newArrayList(context.getUser()), promoteToFirst) { @Override int compare(ResolveInfo lhs, ResolveInfo rhs) { @@ -94,7 +128,7 @@ public class AbstractResolverComparatorTest { void doCompute(List<ResolvedComponentInfo> targets) {} @Override - public float getScore(ComponentName name) { + public float getScore(TargetInfo targetInfo) { return 0; } diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index 0c817cb2..742aac71 100644 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -26,7 +26,9 @@ import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ShortcutManager import android.os.UserHandle import android.os.UserManager +import androidx.lifecycle.Lifecycle import androidx.test.filters.SmallTest +import com.android.intentresolver.TestLifecycleOwner import com.android.intentresolver.any import com.android.intentresolver.argumentCaptor import com.android.intentresolver.capture @@ -36,19 +38,27 @@ import com.android.intentresolver.createShareShortcutInfo import com.android.intentresolver.createShortcutInfo import com.android.intentresolver.mock import com.android.intentresolver.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test import org.mockito.Mockito.anyInt import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.util.concurrent.Executor import java.util.function.Consumer +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest class ShortcutLoaderTest { private val appInfo = ApplicationInfo().apply { @@ -58,7 +68,7 @@ class ShortcutLoaderTest { private val pm = mock<PackageManager> { whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) } - val userManager = mock<UserManager> { + private val userManager = mock<UserManager> { whenever(isUserRunning(any<UserHandle>())).thenReturn(true) whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true) whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false) @@ -68,32 +78,46 @@ class ShortcutLoaderTest { whenever(createContextAsUser(any(), anyInt())).thenReturn(this) whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager) } - private val executor = ImmediateExecutor() + private val scheduler = TestCoroutineScheduler() + private val dispatcher = UnconfinedTestDispatcher(scheduler) + private val lifecycleOwner = TestLifecycleOwner() private val intentFilter = mock<IntentFilter>() private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() private val callback = mock<Consumer<ShortcutLoader.Result>>() + private val componentName = ComponentName("pkg", "Class") + private val appTarget = mock<DisplayResolveInfo> { + whenever(resolvedComponentName).thenReturn(componentName) + } + private val appTargets = arrayOf(appTarget) + private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } @Test - fun test_queryShortcuts_result_consistency_with_AppPredictor() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock<DisplayResolveInfo> { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) + fun test_loadShortcutsWithAppPredictor_resultIntegrity() { val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val matchingAppTarget = createAppTarget(matchingShortcutInfo) val shortcuts = listOf( matchingAppTarget, @@ -130,13 +154,7 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_result_consistency_with_ShortcutManager() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock<DisplayResolveInfo> { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + fun test_loadShortcutsWithShortcutManager_resultIntegrity() { val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -148,16 +166,16 @@ class ShortcutLoaderTest { whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, null, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) val resultCaptor = argumentCaptor<ShortcutLoader.Result>() verify(callback, times(1)).accept(capture(resultCaptor)) @@ -181,13 +199,7 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock<DisplayResolveInfo> { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() { val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -199,16 +211,16 @@ class ShortcutLoaderTest { whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) verify(appPredictor, times(1)).requestPredictionUpdate() val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() @@ -238,32 +250,154 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() { + fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() { + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock<ShortcutManager> { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + whenever(appPredictor.requestPredictionUpdate()) + .thenThrow(IllegalStateException("Test exception")) + val testSubject = ShortcutLoader( + context, + lifecycleOwner.lifecycle, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + testSubject.updateAppTargets(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) + + val result = resultCaptor.value + assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) + assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) + assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) + assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertTrue( + "AppTargets are not expected the cache of a ShortcutManager result", + result.directShareAppTargetCache.isEmpty() + ) + assertEquals( + "Wrong ShortcutInfo in the cache", + matchingShortcutInfo, + result.directShareShortcutInfoCache[shortcut] + ) + } + } + + @Test + fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() { + ShortcutLoader( + context, + lifecycleOwner.lifecycle, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(appPredictor, times(1)).requestPredictionUpdate() + verify(callback, never()).accept(any()) + } + + @Test + fun test_ShortcutLoader_noResultsWithoutAppTargets() { + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock<ShortcutManager> { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = ShortcutLoader( + context, + lifecycleOwner.lifecycle, + null, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(shortcutManager, times(1)).getShareTargets(any()) + verify(callback, never()).accept(any()) + + testSubject.reset() + + verify(shortcutManager, times(2)).getShareTargets(any()) + verify(callback, never()).accept(any()) + + testSubject.updateAppTargets(appTargets) + + verify(shortcutManager, times(2)).getShareTargets(any()) + verify(callback, times(1)).accept(any()) + } + + @Test + fun test_OnLifecycleDestroyed_unsubscribeFromAppPredictor() { + ShortcutLoader( + context, + lifecycleOwner.lifecycle, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(appPredictor, never()).unregisterPredictionUpdates(any()) + + lifecycleOwner.state = Lifecycle.State.DESTROYED + + verify(appPredictor, times(1)).unregisterPredictionUpdates(any()) + } + + @Test + fun test_workProfileNotRunning_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) } @Test - fun test_queryShortcuts_do_not_call_services_for_locked_work_profile() { + fun test_workProfileLocked_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) } @Test - fun test_queryShortcuts_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { + fun test_workProfileQuiteModeEnabled_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) } @Test - fun test_queryShortcuts_call_services_for_not_running_main_profile() { + fun test_mainProfileNotRunning_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isUserRunning = false) } @Test - fun test_queryShortcuts_call_services_for_locked_main_profile() { + fun test_mainProfileLocked_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isUserUnlocked = false) } @Test - fun test_queryShortcuts_call_services_if_quite_mode_is_enabled_for_main_profile() { + fun test_mainProfileQuiteModeEnabled_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) } @@ -283,16 +417,16 @@ class ShortcutLoaderTest { val callback = mock<Consumer<ShortcutLoader.Result>>() val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, userHandle, false, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) verify(appPredictor, never()).requestPredictionUpdate() } @@ -313,23 +447,17 @@ class ShortcutLoaderTest { val callback = mock<Consumer<ShortcutLoader.Result>>() val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, userHandle, true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) verify(appPredictor, times(1)).requestPredictionUpdate() } } - -private class ImmediateExecutor : Executor { - override fun execute(r: Runnable) { - r.run() - } -} diff --git a/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt b/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt new file mode 100644 index 00000000..18218064 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt @@ -0,0 +1,95 @@ +package com.android.intentresolver.util + +import android.app.PendingIntent +import android.content.IIntentReceiver +import android.content.IIntentSender +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.net.Uri +import android.os.Binder +import android.os.Bundle +import android.os.IBinder +import android.os.UserHandle +import android.service.chooser.ChooserAction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class UriFiltersTest { + + @Test + fun uri_ownedByCurrentUser_noUserId() { + val uri = Uri.parse("content://media/images/12345") + assertTrue("Uri without userId should always return true", uri.ownedByCurrentUser) + } + + @Test + fun uri_ownedByCurrentUser_selfUserId() { + val uri = Uri.parse("content://${UserHandle.myUserId()}@media/images/12345") + assertTrue("Uri with own userId should return true", uri.ownedByCurrentUser) + } + + @Test + fun uri_ownedByCurrentUser_otherUserId() { + val otherUserId = UserHandle.myUserId() + 10 + val uri = Uri.parse("content://${otherUserId}@media/images/12345") + assertFalse("Uri with other userId should return false", uri.ownedByCurrentUser) + } + + @Test + fun chooserAction_hasValidIcon_bitmap() = + smallBitmap().use { + val icon = Icon.createWithBitmap(it) + val action = actionWithIcon(icon) + assertTrue("No uri, assumed valid", hasValidIcon(action)) + } + + @Test + fun chooserAction_hasValidIcon_uri() { + val icon = Icon.createWithContentUri("content://provider/content/12345") + assertTrue("No userId in uri, uri is valid", hasValidIcon(actionWithIcon(icon))) + } + @Test + fun chooserAction_hasValidIcon_uri_unowned() { + val userId = UserHandle.myUserId() + 10 + val icon = Icon.createWithContentUri("content://${userId}@provider/content/12345") + assertFalse("uri userId references a different user", hasValidIcon(actionWithIcon(icon))) + } + + private fun smallBitmap() = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) + + private fun mockAction(): PendingIntent { + return PendingIntent( + object : IIntentSender { + override fun asBinder(): IBinder = Binder() + override fun send( + code: Int, + intent: Intent?, + resolvedType: String?, + whitelistToken: IBinder?, + finishedReceiver: IIntentReceiver?, + requiredPermission: String?, + options: Bundle? + ) { + /* empty */ + } + } + ) + } + + private fun actionWithIcon(icon: Icon): ChooserAction { + return ChooserAction.Builder(icon, "", mockAction()).build() + } + + /** Unconditionally recycles the [Bitmap] after running the given block */ + private fun Bitmap.use(block: (Bitmap) -> Unit) = + try { + block(this) + } finally { + recycle() + } +} diff --git a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt new file mode 100644 index 00000000..e65cba5f --- /dev/null +++ b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2023 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.widget + +import android.graphics.Bitmap +import android.net.Uri +import com.android.intentresolver.captureMany +import com.android.intentresolver.mock +import com.android.intentresolver.widget.ScrollableImagePreviewView.BatchPreviewLoader +import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview +import com.android.intentresolver.widget.ScrollableImagePreviewView.PreviewType +import com.android.intentresolver.withArgCaptor +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.atLeast +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +class BatchPreviewLoaderTest { + private val dispatcher = UnconfinedTestDispatcher() + private val testScope = CoroutineScope(dispatcher) + private val onCompletion = mock<() -> Unit>() + private val onReset = mock<(Int) -> Unit>() + private val onUpdate = mock<(List<Preview>) -> Unit>() + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + } + + @After + fun cleanup() { + testScope.cancel() + Dispatchers.resetMain() + } + + @Test + fun test_allImagesWithinViewPort_oneUpdate() { + val imageLoader = TestImageLoader(testScope) + val uriOne = createUri(1) + val uriTwo = createUri(2) + imageLoader.setUriLoadingOrder(succeed(uriTwo), succeed(uriOne)) + val testSubject = + BatchPreviewLoader( + imageLoader, + previews(uriOne, uriTwo), + 0, + onReset, + onUpdate, + onCompletion + ) + testSubject.loadAspectRatios(200) { _, _, _ -> 100 } + dispatcher.scheduler.advanceUntilIdle() + + verify(onCompletion, times(1)).invoke() + verify(onReset, times(1)).invoke(2) + val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri } + assertThat(list).containsExactly(uriOne, uriTwo).inOrder() + } + + @Test + fun test_allImagesWithinViewPortOneFailed_failedPreviewIsNotUpdated() { + val imageLoader = TestImageLoader(testScope) + val uriOne = createUri(1) + val uriTwo = createUri(2) + val uriThree = createUri(3) + imageLoader.setUriLoadingOrder(succeed(uriThree), fail(uriTwo), succeed(uriOne)) + val testSubject = + BatchPreviewLoader( + imageLoader, + previews(uriOne, uriTwo, uriThree), + 0, + onReset, + onUpdate, + onCompletion + ) + testSubject.loadAspectRatios(200) { _, _, _ -> 100 } + dispatcher.scheduler.advanceUntilIdle() + + verify(onCompletion, times(1)).invoke() + verify(onReset, times(1)).invoke(3) + val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri } + assertThat(list).containsExactly(uriOne, uriThree).inOrder() + } + + @Test + fun test_imagesLoadedNotInOrder_updatedInOrder() { + val imageLoader = TestImageLoader(testScope) + val uris = Array(10) { createUri(it) } + val loadingOrder = + Array(uris.size) { i -> + val uriIdx = + when { + i % 2 == 1 -> i - 1 + i % 2 == 0 && i < uris.size - 1 -> i + 1 + else -> i + } + succeed(uris[uriIdx]) + } + imageLoader.setUriLoadingOrder(*loadingOrder) + val testSubject = + BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion) + testSubject.loadAspectRatios(200) { _, _, _ -> 100 } + dispatcher.scheduler.advanceUntilIdle() + + verify(onCompletion, times(1)).invoke() + verify(onReset, times(1)).invoke(uris.size) + val list = + captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) } + .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } } + .map { it.uri } + assertThat(list).containsExactly(*uris).inOrder() + } + + @Test + fun test_imagesLoadedNotInOrderSomeFailed_updatedInOrder() { + val imageLoader = TestImageLoader(testScope) + val uris = Array(10) { createUri(it) } + val loadingOrder = + Array(uris.size) { i -> + val uriIdx = + when { + i % 2 == 1 -> i - 1 + i % 2 == 0 && i < uris.size - 1 -> i + 1 + else -> i + } + if (uriIdx % 2 == 0) fail(uris[uriIdx]) else succeed(uris[uriIdx]) + } + val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) } + imageLoader.setUriLoadingOrder(*loadingOrder) + val testSubject = + BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion) + testSubject.loadAspectRatios(200) { _, _, _ -> 100 } + dispatcher.scheduler.advanceUntilIdle() + + verify(onCompletion, times(1)).invoke() + verify(onReset, times(1)).invoke(uris.size) + val list = + captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) } + .fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } } + .map { it.uri } + assertThat(list).containsExactly(*expectedUris).inOrder() + } + + private fun createUri(idx: Int): Uri = Uri.parse("content://org.pkg.app/image-$idx.png") + + private fun fail(uri: Uri) = uri to false + private fun succeed(uri: Uri) = uri to true + private fun previews(vararg uris: Uri) = + uris.fold(ArrayList<Preview>(uris.size)) { acc, uri -> + acc.apply { add(Preview(PreviewType.Image, uri, editAction = null)) } + } +} + +private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? { + private val loadingOrder = ArrayDeque<Pair<Uri, Boolean>>() + private val pendingRequests = LinkedHashMap<Uri, CompletableDeferred<Bitmap?>>() + private val flow = MutableSharedFlow<Unit>(replay = 1) + private val bitmap by lazy { Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888) } + + init { + scope.launch { + flow.collect { + while (true) { + val (nextUri, isLoaded) = loadingOrder.firstOrNull() ?: break + val deferred = pendingRequests.remove(nextUri) ?: break + loadingOrder.removeFirst() + deferred.complete(if (isLoaded) bitmap else null) + } + if (loadingOrder.isEmpty()) { + pendingRequests.forEach { (uri, deferred) -> deferred.complete(bitmap) } + pendingRequests.clear() + } + } + } + } + + fun setUriLoadingOrder(vararg uris: Pair<Uri, Boolean>) { + loadingOrder.clear() + loadingOrder.addAll(uris) + } + + override suspend fun invoke(uri: Uri, cache: Boolean): Bitmap? { + val deferred = pendingRequests.getOrPut(uri) { CompletableDeferred() } + flow.tryEmit(Unit) + return deferred.await() + } +} |