diff options
| author | 2023-04-18 16:34:38 -0700 | |
|---|---|---|
| committer | 2023-04-18 16:34:38 -0700 | |
| commit | 4042e26988acfecda45dbcc4d01ac1be7813b42e (patch) | |
| tree | 970a428d34bfaad9fd8e70ad12619586033d18a7 /java/tests | |
| parent | d36ad81bb94eb3178d53a227c98692e559465ea2 (diff) | |
| parent | 4bc99bb4b351fe2304b3c7e147248a28c507ae57 (diff) | |
Merge Android 13 QPR3 tm-qpr-dev-plus-aosp-without-vendor@9936994
Bug: 275386652
Merged-In: I392de610b3d3e044e23c83d29fd11061fbc7192d
Change-Id: Ib64b6b991713c518faaab01935cad9e8a57e0d98
Diffstat (limited to 'java/tests')
25 files changed, 2279 insertions, 550 deletions
diff --git a/java/tests/Android.bp b/java/tests/Android.bp index 2913d128..4e835ec8 100644 --- a/java/tests/Android.bp +++ b/java/tests/Android.bp @@ -21,11 +21,16 @@ android_test { "IntentResolver-core", "androidx.test.rules", "androidx.test.ext.junit", + "androidx.test.espresso.contrib", "mockito-target-minus-junit4", "androidx.test.espresso.core", + "androidx.lifecycle_lifecycle-common-java8", + "androidx.lifecycle_lifecycle-extensions", + "androidx.lifecycle_lifecycle-runtime-ktx", "truth-prebuilt", "testables", "testng", + "kotlinx_coroutines_test", ], test_suites: ["general-tests"], sdk_version: "core_platform", diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt new file mode 100644 index 00000000..af134fcd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -0,0 +1,154 @@ +/* + * 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.Activity +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Resources +import android.graphics.drawable.Icon +import android.service.chooser.ChooserAction +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 +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import java.util.concurrent.Callable +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.function.Consumer + +@RunWith(AndroidJUnit4::class) +class ChooserActionFactoryTest { + private val context = InstrumentationRegistry.getInstrumentation().getContext() + + private val logger = mock<ChooserActivityLogger>() + private val flags = mock<FeatureFlagRepository>() + private val actionLabel = "Action label" + private val testAction = "com.android.intentresolver.testaction" + private val countdown = CountDownLatch(1) + private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + // Just doing at most a single countdown per test. + countdown.countDown() + } + } + private object resultConsumer : Consumer<Int> { + var latestReturn = Integer.MIN_VALUE + + override fun accept(resultCode: Int) { + latestReturn = resultCode + } + + } + + @Before + fun setup() { + whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(true) + context.registerReceiver(testReceiver, IntentFilter(testAction)) + } + + @After + fun teardown() { + context.unregisterReceiver(testReceiver) + } + + @Test + fun testCreateCustomActions() { + val factory = createFactory() + + val customActions = factory.createCustomActions() + + assertThat(customActions.size).isEqualTo(1) + assertThat(customActions[0].label).isEqualTo(actionLabel) + + // click it + customActions[0].onClicked.run() + + Mockito.verify(logger).logCustomActionSelected(eq(0)) + assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) + // Verify the pendingintent has been called + countdown.await(500, TimeUnit.MILLISECONDS) + } + + @Test + fun testNoModifyShareAction() { + val factory = createFactory(includeModifyShare = false) + + assertThat(factory.modifyShareAction).isNull() + } + + @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() + + Mockito.verify(logger).logActionSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) + assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn) + // Verify the pendingintent has been called + countdown.await(500, TimeUnit.MILLISECONDS) + } + + private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory { + val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction),0) + val targetIntent = Intent() + val action = ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + actionLabel, + testPendingIntent + ).build() + val chooserRequest = mock<ChooserRequestParameters>() + whenever(chooserRequest.targetIntent).thenReturn(targetIntent) + whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) + + if (includeModifyShare) { + whenever(chooserRequest.modifyShareAction).thenReturn(testPendingIntent) + } + + return ChooserActionFactory( + context, + chooserRequest, + flags, + mock<ChooserIntegratedDeviceComponents>(), + logger, + Consumer<Boolean>{}, + Callable<View?>{null}, + mock<ChooserActionFactory.ActionActivityStarter>(), + resultConsumer) + } +}
\ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java index 705a3228..aa42c24c 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java @@ -36,6 +36,7 @@ import com.android.intentresolver.ChooserActivityLogger.FrameworkStatsLogger; import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent; import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent; import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent; +import com.android.intentresolver.contentpreview.ContentPreviewType; import com.android.internal.logging.InstanceId; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; @@ -112,24 +113,26 @@ public final class ChooserActivityLoggerTest { @Test public void testLogShareStarted() { - final int eventId = -1; // Passed-in eventId is unused. TODO: remove from method signature. final String packageName = "com.test.foo"; final String mimeType = "text/plain"; final int appProvidedDirectTargets = 123; final int appProvidedAppTargets = 456; final boolean workProfile = true; - final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_FILE; + final int previewType = ContentPreviewType.CONTENT_PREVIEW_FILE; final String intentAction = Intent.ACTION_SENDTO; + final int numCustomActions = 3; + final boolean modifyShareProvided = true; mChooserLogger.logShareStarted( - eventId, packageName, mimeType, appProvidedDirectTargets, appProvidedAppTargets, workProfile, previewType, - intentAction); + intentAction, + numCustomActions, + modifyShareProvided); verify(mFrameworkLog).write( eq(FrameworkStatsLog.SHARESHEET_STARTED), @@ -141,7 +144,9 @@ public final class ChooserActivityLoggerTest { eq(appProvidedAppTargets), eq(workProfile), eq(FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE), - eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO)); + eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO), + /* custom actions provided */ eq(numCustomActions), + /* reselection action provided */ eq(modifyShareProvided)); } @Test @@ -203,6 +208,17 @@ public final class ChooserActivityLoggerTest { } @Test + public void testLogCustomActionSelected() { + final int position = 4; + mChooserLogger.logCustomActionSelected(position); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.RANKING_SELECTED), + eq(SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId()), + any(), anyInt(), eq(position), eq(false)); + } + + @Test public void testLogDirectShareTargetReceived() { final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER; final int latency = 123; @@ -218,7 +234,7 @@ public final class ChooserActivityLoggerTest { @Test public void testLogActionShareWithPreview() { - final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT; + final int previewType = ContentPreviewType.CONTENT_PREVIEW_TEXT; mChooserLogger.logActionShareWithPreview(previewType); diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index 5df0d4a2..f0c459e5 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -29,8 +29,8 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.shortcuts.ShortcutLoader; import java.util.function.Consumer; @@ -58,8 +58,8 @@ public class ChooserActivityOverrideData { public Function<TargetInfo, Boolean> onSafelyStartCallback; public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> shortcutLoaderFactory = (userHandle, callback) -> null; - public ResolverListController resolverListController; - public ResolverListController workResolverListController; + public ChooserActivity.ChooserListController resolverListController; + public ChooserActivity.ChooserListController workResolverListController; public Boolean isVoiceInteraction; public boolean isImageType; public Cursor resolverCursor; @@ -72,10 +72,11 @@ public class ChooserActivityOverrideData { public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public Integer myUserId; - public QuietModeManager mQuietModeManager; + public WorkProfileAvailabilityManager mWorkProfileAvailability; public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public PackageManager packageManager; + public FeatureFlagRepository featureFlagRepository; public void reset() { onSafelyStartCallback = null; @@ -85,8 +86,8 @@ public class ChooserActivityOverrideData { isImageType = false; resolverCursor = null; resolverForceException = false; - resolverListController = mock(ResolverListController.class); - workResolverListController = mock(ResolverListController.class); + resolverListController = mock(ChooserActivity.ChooserListController.class); + workResolverListController = mock(ChooserActivity.ChooserListController.class); chooserActivityLogger = mock(ChooserActivityLogger.class); alternateProfileSetting = 0; resources = null; @@ -95,23 +96,26 @@ public class ChooserActivityOverrideData { isQuietModeEnabled = false; myUserId = null; packageManager = null; - mQuietModeManager = new QuietModeManager() { + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { @Override - public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { + public boolean isQuietModeEnabled() { return isQuietModeEnabled; } @Override - public void requestQuietModeEnabled(boolean enabled, - UserHandle workProfileUserHandle) { - isQuietModeEnabled = enabled; + public boolean isWorkProfileUserUnlocked() { + return true; } @Override - public void markWorkProfileEnabledBroadcastReceived() { + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; } @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override public boolean isWaitingToEnableWorkProfile() { return false; } @@ -128,6 +132,7 @@ public class ChooserActivityOverrideData { mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) .thenAnswer(invocation -> hasCrossProfileIntents); + featureFlagRepository = null; } private ChooserActivityOverrideData() {} diff --git a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt b/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt new file mode 100644 index 00000000..9a5dabdb --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt @@ -0,0 +1,71 @@ +/* + * 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.ComponentName +import android.provider.Settings +import android.testing.TestableContext +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 ChooserIntegratedDeviceComponentsTest { + private val secureSettings = mock<SecureSettings>() + private val testableContext = + TestableContext(InstrumentationRegistry.getInstrumentation().getContext()) + + @Test + fun testEditorAndNearby() { + val resources = testableContext.getOrCreateTestableResources() + + resources.addOverride(R.string.config_systemImageEditor, "") + resources.addOverride(R.string.config_defaultNearbySharingComponent, "") + + var components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) + + assertThat(components.editSharingComponent).isNull() + assertThat(components.nearbySharingComponent).isNull() + + val editor = ComponentName.unflattenFromString("com.android/com.android.Editor") + val nearby = ComponentName.unflattenFromString("com.android/com.android.nearby") + + resources.addOverride(R.string.config_systemImageEditor, editor?.flattenToString()) + resources.addOverride( + R.string.config_defaultNearbySharingComponent, nearby?.flattenToString()) + + components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) + + assertThat(components.editSharingComponent).isEqualTo(editor) + assertThat(components.nearbySharingComponent).isEqualTo(nearby) + + val anotherNearby = + ComponentName.unflattenFromString("com.android/com.android.another_nearby") + whenever( + secureSettings.getString( + any(), + eq(Settings.Secure.NEARBY_SHARING_COMPONENT) + ) + ).thenReturn(anotherNearby?.flattenToString()) + + components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings) + + assertThat(components.nearbySharingComponent).isEqualTo(anotherNearby) + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt new file mode 100644 index 00000000..50c37c7f --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.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 + +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.intentresolver.chooser.TargetInfo +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) +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) + } + + refinementManager.maybeHandleSelection(targetInfo) + + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + 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)) + + val alternates = + intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java) + assertEquals(1, alternates?.size) + assertEquals(intents[1], alternates?.get(0)) + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 97de97f5..d4ae666b 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -16,8 +16,6 @@ package com.android.intentresolver; -import static org.mockito.Mockito.when; - import android.annotation.Nullable; import android.app.prediction.AppPredictor; import android.app.usage.UsageStatsManager; @@ -30,17 +28,14 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.Bitmap; import android.net.Uri; import android.os.UserHandle; -import android.util.Size; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -121,15 +116,13 @@ public class ChooserWrapperActivity } @Override - protected ComponentName getNearbySharingComponent() { - // an arbitrary pre-installed activity that handles this type of intent - return ComponentName.unflattenFromString("com.google.android.apps.messaging/" - + "com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity"); - } - - @Override - protected TargetInfo getNearbySharingTarget(Intent originalIntent) { - return NotSelectableTargetInfo.newEmptyTargetInfo(); + protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { + return new ChooserIntegratedDeviceComponents( + /* editSharingComponent=*/ null, + // An arbitrary pre-installed activity that handles this type of intent: + /* nearbySharingComponent=*/ new ComponentName( + "com.google.android.apps.messaging", + ".ui.conversationlist.ShareIntentActivity")); } @Override @@ -165,15 +158,15 @@ public class ChooserWrapperActivity } @Override - protected QuietModeManager createQuietModeManager() { - if (sOverrides.mQuietModeManager != null) { - return sOverrides.mQuietModeManager; + protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { + if (sOverrides.mWorkProfileAvailability != null) { + return sOverrides.mWorkProfileAvailability; } - return super.createQuietModeManager(); + return super.createWorkProfileAvailabilityManager(); } @Override - public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) { + public void safelyStartActivity(TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { return; @@ -182,12 +175,10 @@ public class ChooserWrapperActivity } @Override - protected ResolverListController createListController(UserHandle userHandle) { + protected ChooserListController createListController(UserHandle userHandle) { if (userHandle == UserHandle.SYSTEM) { - when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); return sOverrides.resolverListController; } - when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); return sOverrides.workResolverListController; } @@ -208,11 +199,10 @@ public class ChooserWrapperActivity } @Override - protected Bitmap loadThumbnail(Uri uri, Size size) { - if (sOverrides.previewThumbnail != null) { - return sOverrides.previewThumbnail; - } - return super.loadThumbnail(uri, size); + protected ImageLoader createPreviewImageLoader() { + return new TestPreviewImageLoader( + super.createPreviewImageLoader(), + () -> sOverrides.previewThumbnail); } @Override @@ -290,4 +280,12 @@ public class ChooserWrapperActivity return super.createShortcutLoader( context, appPredictor, userHandle, targetIntentFilter, callback); } + + @Override + protected FeatureFlagRepository createFeatureFlagRepository() { + if (sOverrides.featureFlagRepository != null) { + return sOverrides.featureFlagRepository; + } + return super.createFeatureFlagRepository(); + } } diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt new file mode 100644 index 00000000..9ea9dfa7 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt @@ -0,0 +1,112 @@ +/* + * 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.res.Resources +import android.view.View +import android.view.Window +import androidx.activity.ComponentActivity +import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +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.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +private const val TIMEOUT_MS = 200 + +@OptIn(ExperimentalCoroutinesApi::class) +class EnterTransitionAnimationDelegateTest { + private val elementName = "shared-element" + private val scheduler = TestCoroutineScheduler() + private val dispatcher = StandardTestDispatcher(scheduler) + private val lifecycleOwner = TestLifecycleOwner() + + private val transitionTargetView = mock<View> { + // avoid the request-layout path in the delegate + whenever(isInLayout).thenReturn(true) + } + + private val windowMock = mock<Window>() + private val resourcesMock = mock<Resources> { + whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS) + } + private val activity = mock<ComponentActivity> { + whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle) + whenever(resources).thenReturn(resourcesMock) + whenever(isActivityTransitionRunning).thenReturn(true) + whenever(window).thenReturn(windowMock) + } + + private val testSubject = EnterTransitionAnimationDelegate(activity) { + transitionTargetView + } + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } + + @Test + fun test_postponeTransition_timeout() { + testSubject.postponeTransition() + testSubject.markOffsetCalculated() + + scheduler.advanceTimeBy(TIMEOUT_MS + 1L) + verify(activity, times(1)).startPostponedEnterTransition() + verify(windowMock, never()).setWindowAnimations(anyInt()) + } + + @Test + fun test_postponeTransition_animation_resumes_only_once() { + testSubject.postponeTransition() + testSubject.markOffsetCalculated() + testSubject.onTransitionElementReady(elementName) + testSubject.markOffsetCalculated() + testSubject.onTransitionElementReady(elementName) + + scheduler.advanceTimeBy(TIMEOUT_MS + 1L) + verify(activity, times(1)).startPostponedEnterTransition() + } + + @Test + fun test_postponeTransition_resume_animation_conditions() { + testSubject.postponeTransition() + verify(activity, never()).startPostponedEnterTransition() + + testSubject.markOffsetCalculated() + verify(activity, never()).startPostponedEnterTransition() + + testSubject.onAllTransitionElementsReady() + verify(activity, times(1)).startPostponedEnterTransition() + } +} diff --git a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt new file mode 100644 index 00000000..3fa01bcc --- /dev/null +++ b/java/tests/src/com/android/intentresolver/FeatureFlagRule.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 com.android.systemui.flags.BooleanFlag +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Ignores tests annotated with [RequireFeatureFlags] which flag requirements does not + * meet in the active flag set. + * @param flags active flag set + */ +internal class FeatureFlagRule(flags: Map<BooleanFlag, Boolean>) : TestRule { + private val flags = flags.entries.fold(HashMap<String, Boolean>()) { map, (key, value) -> + map.apply { + put(key.name, value) + } + } + private val skippingStatement = object : Statement() { + override fun evaluate() = Unit + } + + override fun apply(base: Statement, description: Description): Statement { + val annotation = description.annotations.firstOrNull { + it is RequireFeatureFlags + } as? RequireFeatureFlags + ?: return base + + if (annotation.flags.size != annotation.values.size) { + error("${description.className}#${description.methodName}: inconsistent number of" + + " flags and values in $annotation") + } + for (i in annotation.flags.indices) { + val flag = annotation.flags[i] + val value = annotation.values[i] + if (flags.getOrDefault(flag, !value) != value) return skippingStatement + } + return base + } +} diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt new file mode 100644 index 00000000..f327e19e --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt @@ -0,0 +1,101 @@ +/* + * 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/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt index 159c6d6a..aaa7a282 100644 --- a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt +++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt @@ -26,6 +26,7 @@ package com.android.intentresolver import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers import org.mockito.Mockito import org.mockito.stubbing.OngoingStubbing @@ -144,3 +145,5 @@ inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> */ inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = kotlinArgumentCaptor<T>().apply{ block() }.allValues + +inline fun <reified T> anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher<T?> { true }) diff --git a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt new file mode 100644 index 00000000..1ddf7462 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt @@ -0,0 +1,23 @@ +/* + * 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 + +/** + * Specifies expected feature flag values for a test. + */ +@Target(AnnotationTarget.FUNCTION) +annotation class RequireFeatureFlags(val flags: Array<String>, val values: BooleanArray) diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index 62c16ff5..ae1b99f8 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -54,7 +54,6 @@ import androidx.test.espresso.NoMatchingViewException; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; -import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.R; @@ -101,10 +100,7 @@ public class ResolverActivityTest { Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); @@ -133,10 +129,7 @@ public class ResolverActivityTest { Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); @@ -178,10 +171,7 @@ public class ResolverActivityTest { Intent sendIntent = createSendImageIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); @@ -211,10 +201,7 @@ public class ResolverActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); when(sOverrides.resolverListController.getLastChosen()) .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); @@ -277,10 +264,7 @@ public class ResolverActivityTest { createResolvedComponentsForTestWithOtherProfile(3); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); @@ -319,10 +303,7 @@ public class ResolverActivityTest { createResolvedComponentsForTestWithOtherProfile(3); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); when(sOverrides.resolverListController.getLastChosen()) .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); @@ -761,10 +742,7 @@ public class ResolverActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); when(sOverrides.resolverListController.getLastChosen()) .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); @@ -837,22 +815,33 @@ public class ResolverActivityTest { } private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + when(sOverrides.resolverListController.getResolversForIntentAsUser( Mockito.anyBoolean(), Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); - when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); - when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.isA(List.class), eq(UserHandle.SYSTEM))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.of(10)))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); } } diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index fb928e09..b6b32b5a 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -36,20 +36,33 @@ public class ResolverDataProvider { static private int USER_SOMEONE_ELSE = 10; - static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo(int i) { - return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), - createResolverIntent(i), createResolveInfo(i, UserHandle.USER_CURRENT)); + static ResolvedComponentInfo createResolvedComponentInfo(int i) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, UserHandle.USER_CURRENT)); } - static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { - return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), - createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE)); + static ResolvedComponentInfo createResolvedComponentInfo( + ComponentName componentName, Intent intent) { + return new ResolvedComponentInfo( + componentName, + intent, + createResolveInfo(componentName, UserHandle.USER_CURRENT)); } - static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, - int userId) { - return new ResolverActivity.ResolvedComponentInfo(createComponentName(i), - createResolverIntent(i), createResolveInfo(i, userId)); + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, USER_SOMEONE_ELSE)); + } + + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, userId)); } public static ComponentName createComponentName(int i) { @@ -64,6 +77,13 @@ public class ResolverDataProvider { return resolveInfo; } + public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(componentName); + resolveInfo.targetUserId = userId; + return resolveInfo; + } + static ActivityInfo createActivityInfo(int i) { ActivityInfo ai = new ActivityInfo(); ai.name = "activity_name" + i; @@ -75,6 +95,18 @@ public class ResolverDataProvider { return ai; } + static ActivityInfo createActivityInfo(ComponentName componentName) { + ActivityInfo ai = new ActivityInfo(); + ai.name = componentName.getClassName(); + ai.packageName = componentName.getPackageName(); + ai.enabled = true; + ai.exported = true; + ai.permission = null; + ai.applicationInfo = createApplicationInfo(); + ai.applicationInfo.packageName = componentName.getPackageName(); + return ai; + } + static ApplicationInfo createApplicationInfo() { ApplicationInfo ai = new ApplicationInfo(); ai.name = "app_name"; diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java index 239bffe0..d67b73af 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -31,7 +31,6 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.TargetInfo; import java.util.List; @@ -88,11 +87,11 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override - protected QuietModeManager createQuietModeManager() { - if (sOverrides.mQuietModeManager != null) { - return sOverrides.mQuietModeManager; + protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { + if (sOverrides.mWorkProfileAvailability != null) { + return sOverrides.mWorkProfileAvailability; } - return super.createQuietModeManager(); + return super.createWorkProfileAvailabilityManager(); } ResolverWrapperAdapter getAdapter() { @@ -130,10 +129,8 @@ public class ResolverWrapperActivity extends ResolverActivity { @Override protected ResolverListController createListController(UserHandle userHandle) { if (userHandle == UserHandle.SYSTEM) { - when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); return sOverrides.resolverListController; } - when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); return sOverrides.workResolverListController; } @@ -175,7 +172,7 @@ public class ResolverWrapperActivity extends ResolverActivity { public Integer myUserId; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; - public QuietModeManager mQuietModeManager; + public WorkProfileAvailabilityManager mWorkProfileAvailability; public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; @@ -190,23 +187,26 @@ public class ResolverWrapperActivity extends ResolverActivity { hasCrossProfileIntents = true; isQuietModeEnabled = false; - mQuietModeManager = new QuietModeManager() { + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { @Override - public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { + public boolean isQuietModeEnabled() { return isQuietModeEnabled; } @Override - public void requestQuietModeEnabled(boolean enabled, - UserHandle workProfileUserHandle) { - isQuietModeEnabled = enabled; + public boolean isWorkProfileUserUnlocked() { + return true; } @Override - public void markWorkProfileEnabledBroadcastReceived() { + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; } @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override public boolean isWaitingToEnableWorkProfile() { return false; } diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt new file mode 100644 index 00000000..b9047712 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt @@ -0,0 +1,31 @@ +/* + * 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 com.android.intentresolver.flags.FeatureFlagRepository +import com.android.systemui.flags.BooleanFlag +import com.android.systemui.flags.ReleasedFlag +import com.android.systemui.flags.UnreleasedFlag + +class TestFeatureFlagRepository( + private val overrides: Map<BooleanFlag, Boolean> +) : FeatureFlagRepository { + override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag) + override fun isEnabled(flag: ReleasedFlag): Boolean = getValue(flag) + + private fun getValue(flag: BooleanFlag) = overrides.getOrDefault(flag, flag.default) +} diff --git a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt new file mode 100644 index 00000000..f47e343f --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt @@ -0,0 +1,33 @@ +/* + * 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.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry + +internal class TestLifecycleOwner : LifecycleOwner { + private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this) + + override fun getLifecycle(): Lifecycle = lifecycleRegistry + + var state: Lifecycle.State + get() = lifecycle.currentState + set(value) { + lifecycleRegistry.currentState = value + } +}
\ 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 new file mode 100644 index 00000000..cfe041dd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.graphics.Bitmap +import android.net.Uri +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) + } + } + + override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(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 af2557ef..9ffd02d4 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -20,6 +20,7 @@ import static android.app.Activity.RESULT_OK; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.longClick; import static androidx.test.espresso.action.ViewActions.swipeUp; import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; @@ -55,13 +56,16 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.app.PendingIntent; import android.app.usage.UsageStatsManager; +import android.content.BroadcastReceiver; import android.content.ClipData; import android.content.ClipDescription; import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -69,6 +73,7 @@ import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutManager.ShareShortcutInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -79,25 +84,29 @@ import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.service.chooser.ChooserAction; import android.service.chooser.ChooserTarget; import android.util.HashedStringCache; import android.util.Pair; import android.util.SparseArray; import android.view.View; +import android.view.ViewGroup; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; 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.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; -import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.flags.Flags; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.flags.BooleanFlag; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -106,6 +115,8 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.ArgumentCaptor; @@ -117,6 +128,8 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; @@ -150,14 +163,40 @@ public class UnbundledChooserActivityTest { return mock; }; + 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); + + private static final Map<BooleanFlag, Boolean> ALL_FLAGS_OFF = + createAllFlagsOverride(false); + private static final Map<BooleanFlag, Boolean> ALL_FLAGS_ON = + createAllFlagsOverride(true); + @Parameterized.Parameters public static Collection packageManagers() { return Arrays.asList(new Object[][] { - {0, "Default PackageManager", DEFAULT_PM}, - {1, "No App Prediction Service", NO_APP_PREDICTION_SERVICE_PM} + // Default PackageManager and all flags off + { DEFAULT_PM, ALL_FLAGS_OFF }, + // Default PackageManager and 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 + { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_ON } }); } + private static Map<BooleanFlag, Boolean> createAllFlagsOverride(boolean value) { + HashMap<BooleanFlag, Boolean> overrides = new HashMap<>(ALL_FLAGS.size()); + for (BooleanFlag flag : ALL_FLAGS) { + overrides.put(flag, value); + } + return overrides; + } + /* -------- * Subclasses can override the following methods to customize test behavior. * -------- @@ -177,6 +216,8 @@ public class UnbundledChooserActivityTest { .adoptShellPermissionIdentity(); cleanOverrideData(); + ChooserActivityOverrideData.getInstance().featureFlagRepository = + new TestFeatureFlagRepository(mFlags); } /** @@ -209,11 +250,13 @@ public class UnbundledChooserActivityTest { * -------- */ + @Rule + public final TestRule mRule; + // Shared test code references the activity under test as ChooserActivity, the common ancestor // of any (inheritance-based) chooser implementation. For testing purposes, that activity will // usually be cast to IChooserWrapper to expose instrumentation. - @Rule - public ActivityTestRule<ChooserActivity> mActivityRule = + private ActivityTestRule<ChooserActivity> mActivityRule = new ActivityTestRule<>(ChooserActivity.class, false, false) { @Override public ChooserActivity launchActivity(Intent clientIntent) { @@ -240,16 +283,20 @@ public class UnbundledChooserActivityTest { private static final int CONTENT_PREVIEW_IMAGE = 1; private static final int CONTENT_PREVIEW_FILE = 2; private static final int CONTENT_PREVIEW_TEXT = 3; - private Function<PackageManager, PackageManager> mPackageManagerOverride; - private int mTestNum; + + private final Function<PackageManager, PackageManager> mPackageManagerOverride; + private final Map<BooleanFlag, Boolean> mFlags; public UnbundledChooserActivityTest( - int testNum, - String testName, - Function<PackageManager, PackageManager> packageManagerOverride) { + Function<PackageManager, PackageManager> packageManagerOverride, + Map<BooleanFlag, Boolean> flags) { mPackageManagerOverride = packageManagerOverride; - mTestNum = testNum; + mFlags = flags; + + mRule = RuleChain + .outerRule(new FeatureFlagRule(flags)) + .around(mActivityRule); } private void setDeviceConfigProperty( @@ -284,16 +331,7 @@ public class UnbundledChooserActivityTest { Intent viewIntent = createViewTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity( Intent.createChooser(viewIntent, "chooser test")); @@ -308,16 +346,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); waitForIdle(); onView(withId(android.R.id.title)) @@ -329,16 +358,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(android.R.id.title)) @@ -350,16 +370,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntentWithPreview(null, null); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_title)) @@ -374,16 +385,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_title)) @@ -401,16 +403,7 @@ public class UnbundledChooserActivityTest { Uri.parse("tel:(+49)12345789")); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_title)) @@ -428,16 +421,7 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_title)) @@ -451,16 +435,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -506,16 +481,7 @@ public class UnbundledChooserActivityTest { } resolvedComponentInfos.addAll(infosToStack); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -547,16 +513,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -582,16 +539,7 @@ public class UnbundledChooserActivityTest { @Ignore // b/148158199 @Test public void noResultsFromPackageManager() { - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(null); + setupResolverControllers(null); Intent sendIntent = createSendTextIntent(); final ChooserActivity activity = mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -618,16 +566,7 @@ public class UnbundledChooserActivityTest { }; List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); Intent sendIntent = createSendTextIntent(); final ChooserActivity activity = @@ -679,11 +618,7 @@ public class UnbundledChooserActivityTest { createResolvedComponentsForTestWithOtherProfile(3); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen()) .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); @@ -716,11 +651,7 @@ public class UnbundledChooserActivityTest { createResolvedComponentsForTestWithOtherProfile(3); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -745,15 +676,148 @@ 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; + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT")) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().hasExtra(Intent.EXTRA_TEXT)).isFalse(); + } + + @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; + 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), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT")) + ); + + 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.include_text_action)) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); + } + + @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; + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + Intent alternativeIntent = createSendTextIntent(); + final String text = "alternative intent"; + alternativeIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + alternativeIntent) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + launchedIntentRef.set(targetInfo.getTargetIntent()); + return true; + }; + + onView(withText(resolvedComponentInfos.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); + } + + @Test public void copyTextToClipboard() throws Exception { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final ChooserActivity activity = mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -777,11 +841,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -800,11 +860,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -830,11 +886,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -848,7 +900,7 @@ public class UnbundledChooserActivityTest { @Test - public void oneVisibleImagePreview() throws InterruptedException { + public void oneVisibleImagePreview() { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); @@ -861,30 +913,34 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + 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(not(isDisplayed()))); - onView(withId(com.android.internal.R.id.content_preview_image_3_small)) - .check(matches(not(isDisplayed()))); + onView(withId(com.android.internal.R.id.content_preview_image_area)) + .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)); + assertThat( + "image preview view is fully visible", + isDisplayed().matches(visibleViews.get(0))); + }); } @Test - public void twoVisibleImagePreview() throws InterruptedException { + @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); @@ -898,16 +954,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_image_1_large)) @@ -921,7 +968,10 @@ public class UnbundledChooserActivityTest { } @Test - public void threeOrMoreVisibleImagePreview() throws InterruptedException { + @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); @@ -938,16 +988,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_image_1_large)) @@ -961,6 +1002,72 @@ public class UnbundledChooserActivityTest { } @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); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + uris.add(uri); + uris.add(uri); + uris.add(uri); + 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_area)) + .perform(RecyclerViewActions.scrollToLastPosition()) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size())); + }); + } + + @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 String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(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); @@ -1007,11 +1114,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -1036,16 +1139,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -1065,16 +1159,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_filename)) @@ -1099,16 +1184,7 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(com.android.internal.R.id.content_preview_filename)) @@ -1129,16 +1205,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); ChooserActivityOverrideData.getInstance().resolverForceException = true; @@ -1163,16 +1230,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); Cursor cursor = mock(Cursor.class); when(cursor.getCount()).thenReturn(1); @@ -1199,16 +1257,8 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); + when( ChooserActivityOverrideData .getInstance() @@ -1241,16 +1291,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // create test shortcut loader factory, remember loaders and their callbacks SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = @@ -1331,16 +1372,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // create test shortcut loader factory, remember loaders and their callbacks SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = @@ -1425,16 +1457,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // create test shortcut loader factory, remember loaders and their callbacks SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = @@ -1509,16 +1532,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // create test shortcut loader factory, remember loaders and their callbacks SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = @@ -1597,16 +1611,7 @@ public class UnbundledChooserActivityTest { // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // set caller-provided target Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); @@ -1665,6 +1670,91 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_CUSTOM_ACTIONS_NAME }, + values = { true }) + public void testLaunchWithCustomAction() throws InterruptedException { + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); + final String customActionLabel = "Custom Action"; + final String testAction = "test-broadcast-receiver-action"; + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + chooserIntent.putExtra( + Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, + new ChooserAction[] { + new ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + customActionLabel, + PendingIntent.getBroadcast( + testContext, + 123, + new Intent(testAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)) + .build() + }); + // Start activity + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + final CountDownLatch broadcastInvoked = new CountDownLatch(1); + BroadcastReceiver testReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastInvoked.countDown(); + } + }; + testContext.registerReceiver(testReceiver, new IntentFilter(testAction)); + + try { + onView(withText(customActionLabel)).perform(click()); + broadcastInvoked.await(); + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_RESELECTION_ACTION_NAME }, + values = { true }) + public void testLaunchWithShareModification() throws InterruptedException { + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); + final String modifyShareAction = "test-broadcast-receiver-action"; + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + chooserIntent.putExtra( + Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, + PendingIntent.getBroadcast( + testContext, + 123, + new Intent(modifyShareAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)); + // Start activity + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + final CountDownLatch broadcastInvoked = new CountDownLatch(1); + BroadcastReceiver testReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + broadcastInvoked.countDown(); + } + }; + testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction)); + + try { + onView(withText(R.string.select_text)).perform(click()); + broadcastInvoked.await(); + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException { updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4); givenAppTargets(/* appCount= */ 16); @@ -1715,16 +1805,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // Create direct share target List<ChooserTarget> serviceTargets = createDirectShareTargets(1, @@ -2004,16 +2085,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -2045,16 +2117,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // create test shortcut loader factory, remember loaders and their callbacks SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = @@ -2130,21 +2193,69 @@ public class UnbundledChooserActivityTest { /* selectionCost= */ anyLong()); } + @Test + public void testDirectTargetPinningDialog() { + Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + new SparseArray<>(); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> { + Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = + new Pair<>(mock(ShortcutLoader.class), callback); + shortcutLoaders.put(userHandle.getIdentifier(), pair); + return pair.first; + }; + + // Start activity + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)) + .queryShortcuts(appTargets.capture()); + + // send shortcuts + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + // TODO: test another value as well + false, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() + ); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); + + // Long-click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)).perform(longClick()); + waitForIdle(); + + onView(withId(R.id.chooser_dialog_content)).check(matches(isDisplayed())); + } + @Test @Ignore public void testEmptyDirectRowLogging() throws InterruptedException { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); // Start activity final IChooserWrapper activity = (IChooserWrapper) @@ -2168,16 +2279,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); @@ -2239,16 +2341,7 @@ public class UnbundledChooserActivityTest { public void testOneInitialIntent_noAutolaunch() { List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(1); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + setupResolverControllers(personalResolvedComponentInfos); Intent chooserIntent = createChooserIntent(createSendTextIntent(), new Intent[] {new Intent("action.fake")}); ResolveInfo[] chosen = new ResolveInfo[1]; @@ -2374,12 +2467,7 @@ public class UnbundledChooserActivityTest { // Create 4 ranked app targets. List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(4); - when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + setupResolverControllers(personalResolvedComponentInfos); // Create caller target which is duplicate with one of app targets Intent chooserIntent = createChooserIntent(createSendTextIntent(), new Intent[] {new Intent("action.fake")}); @@ -2646,28 +2734,35 @@ public class UnbundledChooserActivityTest { } private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { when( ChooserActivityOverrideData .getInstance() .resolverListController - .getResolversForIntent( + .getResolversForIntentAsUser( Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), - Mockito.isA(List.class))) + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); when( ChooserActivityOverrideData .getInstance() .workResolverListController - .getResolversForIntent( + .getResolversForIntentAsUser( Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(workResolvedComponentInfos)); + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); when( ChooserActivityOverrideData .getInstance() @@ -2677,8 +2772,8 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.isA(List.class), - eq(UserHandle.SYSTEM))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + eq(UserHandle.of(10)))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); } private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) { @@ -2717,16 +2812,7 @@ public class UnbundledChooserActivityTest { private void givenAppTargets(int appCount) { List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(appCount); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); + setupResolverControllers(resolvedComponentInfos); } private void updateMaxTargetsPerRowResource(int targetsPerRow) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java index f1febed2..87dc1b9d 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -48,7 +48,6 @@ import androidx.test.InstrumentationRegistry; import androidx.test.espresso.NoMatchingViewException; import androidx.test.rule.ActivityTestRule; -import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; import com.android.internal.R; @@ -274,21 +273,27 @@ public class UnbundledChooserActivityWorkProfileTest { private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { - when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + when(sOverrides.resolverListController.getResolversForIntentAsUser( Mockito.anyBoolean(), Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); - when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( Mockito.anyBoolean(), - Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); - when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.isA(List.class), eq(UserHandle.SYSTEM))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(WORK_USER_HANDLE))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); } private void waitForIdle() { diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt new file mode 100644 index 00000000..e9c755d3 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt @@ -0,0 +1,497 @@ +/* + * 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 + *3 + * 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.chooser + +import android.app.Activity +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ResolveInfo +import android.os.Bundle +import android.os.UserHandle +import com.android.intentresolver.createShortcutInfo +import com.android.intentresolver.mock +import com.android.intentresolver.ResolverActivity +import com.android.intentresolver.ResolverDataProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ImmutableTargetInfoTest { + 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 displayLabel: CharSequence = "Display Label" + private val extendedInfo: CharSequence = "Extended Info" + private val displayIconHolder: TargetInfo.IconHolder = mock() + private val sourceIntent1 = Intent("source1") + private val sourceIntent2 = Intent("source2") + private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo( + Intent("display1"), + ResolverDataProvider.createResolveInfo(2, 0), + "display1 label", + "display1 extended info", + Intent("display1_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo( + Intent("display2"), + ResolverDataProvider.createResolveInfo(3, 0), + "display2 label", + "display2 extended info", + Intent("display2_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val directShareShortcutInfo = createShortcutInfo( + "shortcutid", ResolverDataProvider.createComponentName(4), 4) + private val directShareAppTarget = AppTarget( + AppTargetId("apptargetid"), + "test.directshare", + "target", + UserHandle.CURRENT) + private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( + Intent("displayresolve"), + ResolverDataProvider.createResolveInfo(5, 0), + "displayresolve label", + "displayresolve extended info", + Intent("display_resolved"), + /* resolveInfoPresentationGetter= */ null) + private val hashProvider: ImmutableTargetInfo.TargetHashProvider = mock() + + @Test + fun testBasicProperties() { // Fields that are reflected back w/o logic. + // TODO: we could consider passing copies of all the values into the builder so that we can + // verify that they're not mutated (e.g. no extras added to the intents). For now that + // should be obvious from the implementation. + val info = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(resolvedIntent) + .setTargetIntent(targetIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .setResolvedComponentName(resolvedComponentName) + .setChooserTargetComponentName(chooserTargetComponentName) + .setResolveInfo(resolveInfo) + .setDisplayLabel(displayLabel) + .setExtendedInfo(extendedInfo) + .setDisplayIconHolder(displayIconHolder) + .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2)) + .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) + .setIsSuspended(true) + .setIsPinned(true) + .setModifiedScore(42.0f) + .setDirectShareShortcutInfo(directShareShortcutInfo) + .setDirectShareAppTarget(directShareAppTarget) + .setDisplayResolveInfo(displayResolveInfo) + .setHashProvider(hashProvider) + .build() + + assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) + assertThat(info.targetIntent).isEqualTo(targetIntent) + assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) + assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) + assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) + assertThat(info.resolveInfo).isEqualTo(resolveInfo) + assertThat(info.displayLabel).isEqualTo(displayLabel) + assertThat(info.extendedInfo).isEqualTo(extendedInfo) + assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) + assertThat(info.allSourceIntents).containsExactly( + resolvedIntent, sourceIntent1, sourceIntent2) + assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) + assertThat(info.isSuspended).isTrue() + assertThat(info.isPinned).isTrue() + assertThat(info.modifiedScore).isEqualTo(42.0f) + assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) + assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) + assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + assertThat(info.hashProvider).isEqualTo(hashProvider) + } + + @Test + fun testToBuilderPreservesBasicProperties() { + // Note this is set up exactly as in `testBasicProperties`, but the assertions will be made + // against a *copy* of the object instead. + val infoToCopyFrom = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(resolvedIntent) + .setTargetIntent(targetIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .setResolvedComponentName(resolvedComponentName) + .setChooserTargetComponentName(chooserTargetComponentName) + .setResolveInfo(resolveInfo) + .setDisplayLabel(displayLabel) + .setExtendedInfo(extendedInfo) + .setDisplayIconHolder(displayIconHolder) + .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2)) + .setAllDisplayTargets(listOf(displayTarget1, displayTarget2)) + .setIsSuspended(true) + .setIsPinned(true) + .setModifiedScore(42.0f) + .setDirectShareShortcutInfo(directShareShortcutInfo) + .setDirectShareAppTarget(directShareAppTarget) + .setDisplayResolveInfo(displayResolveInfo) + .setHashProvider(hashProvider) + .build() + + val info = infoToCopyFrom.toBuilder().build() + + assertThat(info.resolvedIntent).isEqualTo(resolvedIntent) + assertThat(info.targetIntent).isEqualTo(targetIntent) + assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent) + assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName) + assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName) + assertThat(info.resolveInfo).isEqualTo(resolveInfo) + assertThat(info.displayLabel).isEqualTo(displayLabel) + assertThat(info.extendedInfo).isEqualTo(extendedInfo) + assertThat(info.displayIconHolder).isEqualTo(displayIconHolder) + assertThat(info.allSourceIntents).containsExactly( + resolvedIntent, sourceIntent1, sourceIntent2) + assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2) + assertThat(info.isSuspended).isTrue() + assertThat(info.isPinned).isTrue() + assertThat(info.modifiedScore).isEqualTo(42.0f) + assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo) + assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget) + assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo) + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + assertThat(info.hashProvider).isEqualTo(hashProvider) + } + + @Test + fun testBaseIntentToSend_defaultsToResolvedIntent() { + val info = ImmutableTargetInfo.newBuilder().setResolvedIntent(resolvedIntent).build() + assertThat(info.baseIntentToSend.filterEquals(resolvedIntent)).isTrue() + } + + @Test + fun testBaseIntentToSend_fillsInFromReferrerIntent() { + val originalIntent = Intent() + originalIntent.setPackage("original") + + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + referrerFillInIntent.setPackage("referrer") + + val info = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Only fill if empty. + assertThat(info.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN") + } + + @Test + fun testBaseIntentToSend_fillsInFromRefinementIntent() { + val originalIntent = Intent() + originalIntent.putExtra("ORIGINAL", true) + + val refinementIntent = Intent() + refinementIntent.putExtra("REFINEMENT", true) + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .build() + val info = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent) + + assertThat(info.baseIntentToSend.getBooleanExtra("ORIGINAL", false)).isTrue() + assertThat(info.baseIntentToSend.getBooleanExtra("REFINEMENT", false)).isTrue() + } + + @Test + fun testBaseIntentToSend_twoFillInSourcesFavorsRefinementRequest() { + val originalIntent = Intent("REFINE_ME") + originalIntent.setPackage("original") + + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + referrerFillInIntent.setPackage("referrer_pkg") + referrerFillInIntent.setType("test/referrer") + + val infoWithReferrerFillIn = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + val refinementIntent = Intent("REFINE_ME") + refinementIntent.setPackage("original") // Has to match for refinement. + + val info = infoWithReferrerFillIn.tryToCloneWithAppliedRefinement(refinementIntent) + + assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Set all along. + assertThat(info.baseIntentToSend.action).isEqualTo("REFINE_ME") // Refinement wins. + assertThat(info.baseIntentToSend.type).isEqualTo("test/referrer") // Left for referrer. + } + + @Test + fun testBaseIntentToSend_doubleRefinementPreservesReferrerFillInButNotOriginalRefinement() { + val originalIntent = Intent("REFINE_ME") + val referrerFillInIntent = Intent("REFERRER_FILL_IN") + referrerFillInIntent.putExtra("TEST", "REFERRER") + val refinementIntent1 = Intent("REFINE_ME") + refinementIntent1.putExtra("TEST1", "1") + val refinementIntent2 = Intent("REFINE_ME") + refinementIntent2.putExtra("TEST2", "2") + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setReferrerFillInIntent(referrerFillInIntent) + .build() + + val refined1 = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent1) + val refined2 = refined1.tryToCloneWithAppliedRefinement(refinementIntent2) // Cloned clone. + + // Both clones get the same values filled in from the referrer intent. + assertThat(refined1.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER") + assertThat(refined2.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER") + // Each clone has the respective value that was set in their own refinement request. + assertThat(refined1.baseIntentToSend.getStringExtra("TEST1")).isEqualTo("1") + assertThat(refined2.baseIntentToSend.getStringExtra("TEST2")).isEqualTo("2") + // The clones don't have the data from each other's refinements, even though the intent + // field is empty (thus able to be populated by filling-in). + assertThat(refined1.baseIntentToSend.getStringExtra("TEST2")).isNull() + assertThat(refined2.baseIntentToSend.getStringExtra("TEST1")).isNull() + } + + @Test + fun testBaseIntentToSend_refinementToAlternateSourceIntent() { + val originalIntent = Intent("DONT_REFINE_ME") + originalIntent.putExtra("originalIntent", true) + val mismatchedAlternate = Intent("DOESNT_MATCH") + mismatchedAlternate.putExtra("mismatchedAlternate", true) + val targetAlternate = Intent("REFINE_ME") + targetAlternate.putExtra("targetAlternate", true) + val extraMatch = Intent("REFINE_ME") + extraMatch.putExtra("extraMatch", true) + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setAllSourceIntents(listOf( + originalIntent, mismatchedAlternate, targetAlternate, extraMatch)) + .build() + + val refinement = Intent("REFINE_ME") // First match is `targetAlternate` + refinement.putExtra("refinement", true) + + val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement) + assertThat(refinedResult.baseIntentToSend.getBooleanExtra("refinement", false)).isTrue() + assertThat(refinedResult.baseIntentToSend.getBooleanExtra("targetAlternate", false)) + .isTrue() + // None of the other source intents got merged in (not even the later one that matched): + assertThat(refinedResult.baseIntentToSend.getBooleanExtra("originalIntent", false)) + .isFalse() + assertThat(refinedResult.baseIntentToSend.getBooleanExtra("mismatchedAlternate", false)) + .isFalse() + assertThat(refinedResult.baseIntentToSend.getBooleanExtra("extraMatch", false)).isFalse() + } + + @Test + fun testBaseIntentToSend_noSourceIntentMatchingProposedRefinement() { + val originalIntent = Intent("DONT_REFINE_ME") + originalIntent.putExtra("originalIntent", true) + val mismatchedAlternate = Intent("DOESNT_MATCH") + mismatchedAlternate.putExtra("mismatchedAlternate", true) + + val originalInfo = ImmutableTargetInfo.newBuilder() + .setResolvedIntent(originalIntent) + .setAllSourceIntents(listOf(originalIntent, mismatchedAlternate)) + .build() + + val refinement = Intent("PROPOSED_REFINEMENT") + assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() + } + + @Test + fun testLegacySubclassRelationships_empty() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.EMPTY_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isTrue() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isTrue() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_placeholder() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.PLACEHOLDER_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isTrue() + assertThat(info.isNotSelectableTargetInfo).isTrue() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_selectable() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.SELECTABLE_TARGET_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isTrue() + assertThat(info.isChooserTargetInfo).isTrue() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isFalse() + } + + @Test + fun testLegacySubclassRelationships_displayResolveInfo() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.DISPLAY_RESOLVE_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isFalse() + assertThat(info.isDisplayResolveInfo).isTrue() + } + + @Test + fun testLegacySubclassRelationships_multiDisplayResolveInfo() { + val info = ImmutableTargetInfo.newBuilder() + .setLegacyType(ImmutableTargetInfo.LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO) + .build() + + assertThat(info.isEmptyTargetInfo).isFalse() + assertThat(info.isPlaceHolderTargetInfo).isFalse() + assertThat(info.isNotSelectableTargetInfo).isFalse() + assertThat(info.isSelectableTargetInfo).isFalse() + assertThat(info.isChooserTargetInfo).isFalse() + assertThat(info.isMultiDisplayResolveInfo).isTrue() + assertThat(info.isDisplayResolveInfo).isTrue() + } + + @Test + fun testActivityStarter_correctNumberOfInvocations_startAsCaller() { + val activityStarter = object : TestActivityStarter() { + override fun startAsUser( + target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle + ): Boolean { + throw RuntimeException("Wrong API used: startAsUser") + } + } + + val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val activity: ResolverActivity = mock() + val options = Bundle() + options.putInt("TEST_KEY", 1) + + info.startAsCaller(activity, options, 42) + + assertThat(activityStarter.totalInvocations).isEqualTo(1) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) + assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) + assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) + assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) + assertThat(activityStarter.lastInvocationAsCaller).isTrue() + } + + @Test + fun testActivityStarter_correctNumberOfInvocations_startAsUser() { + val activityStarter = object : TestActivityStarter() { + override fun startAsCaller( + target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { + throw RuntimeException("Wrong API used: startAsCaller") + } + } + + val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val activity: Activity = mock() + val options = Bundle() + options.putInt("TEST_KEY", 1) + + info.startAsUser(activity, options, UserHandle.of(42)) + + assertThat(activityStarter.totalInvocations).isEqualTo(1) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info) + assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity) + assertThat(activityStarter.lastInvocationOptions).isEqualTo(options) + assertThat(activityStarter.lastInvocationUserId).isEqualTo(42) + assertThat(activityStarter.lastInvocationAsCaller).isFalse() + } + + @Test + fun testActivityStarter_invokedWithRespectiveTargetInfoAfterCopy() { + val activityStarter = TestActivityStarter() + val info1 = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build() + val info2 = info1.toBuilder().build() + + info1.startAsCaller(mock(), Bundle(), 42) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info1) + info2.startAsCaller(mock(), Bundle(), 42) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) + info2.startAsUser(mock(), Bundle(), UserHandle.of(42)) + assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2) + + assertThat(activityStarter.totalInvocations).isEqualTo(3) // Instance is still shared. + } +} + +private open class TestActivityStarter : ImmutableTargetInfo.TargetActivityStarter { + var totalInvocations = 0 + var lastInvocationTargetInfo: TargetInfo? = null + var lastInvocationActivity: Activity? = null + var lastInvocationOptions: Bundle? = null + var lastInvocationUserId: Integer? = null + var lastInvocationAsCaller = false + + override fun startAsCaller( + target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean { + ++totalInvocations + lastInvocationTargetInfo = target + lastInvocationActivity = activity + lastInvocationOptions = options + lastInvocationUserId = Integer(userId) + lastInvocationAsCaller = true + return true + } + + override fun startAsUser( + target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle): Boolean { + ++totalInvocations + lastInvocationTargetInfo = target + lastInvocationActivity = activity + lastInvocationOptions = options + lastInvocationUserId = Integer(user.identifier) + lastInvocationAsCaller = false + return true + } +} diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt index 7c2b07a9..dddbcccb 100644 --- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt +++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt @@ -22,18 +22,36 @@ import android.content.ComponentName import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ResolveInfo +import android.graphics.drawable.AnimatedVectorDrawable import android.os.UserHandle +import android.test.UiThreadTest import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.ResolverDataProvider import com.android.intentresolver.createChooserTarget import com.android.intentresolver.createShortcutInfo import com.android.intentresolver.mock -import com.android.intentresolver.ResolverDataProvider +import com.android.intentresolver.whenever import com.google.common.truth.Truth.assertThat +import org.junit.Before import org.junit.Test +import org.mockito.Mockito.any +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify class TargetInfoTest { private val context = InstrumentationRegistry.getInstrumentation().getContext() + @Before + fun setup() { + // SelectableTargetInfo reads DeviceConfig and needs a permission for that. + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") + } + @Test fun testNewEmptyTargetInfo() { val info = NotSelectableTargetInfo.newEmptyTargetInfo() @@ -43,13 +61,19 @@ class TargetInfoTest { assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull() } + @UiThreadTest // AnimatedVectorDrawable needs to start from a thread with a Looper. @Test fun testNewPlaceholderTargetInfo() { val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context) - assertThat(info.isPlaceHolderTargetInfo()).isTrue() - assertThat(info.isChooserTargetInfo()).isTrue() // From legacy inheritance model. + assertThat(info.isPlaceHolderTargetInfo).isTrue() + assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model. assertThat(info.hasDisplayIcon()).isTrue() - // TODO: test infrastructure isn't set up to assert anything about the icon itself. + assertThat(info.displayIconHolder.displayIcon) + .isInstanceOf(AnimatedVectorDrawable::class.java) + // TODO: assert that the animation is pre-started/running (IIUC this requires synchronizing + // with some "render thread" per the `AnimatedVectorDrawable` docs). I believe this is + // possible using `AnimatorTestRule` but I couldn't find any sample usage in Kotlin nor get + // it working myself. } @Test @@ -125,6 +149,42 @@ class TargetInfoTest { } @Test + fun testSelectableTargetInfo_noSourceIntentMatchingProposedRefinement() { + val resolvedIntent = Intent("DONT_REFINE_ME") + resolvedIntent.putExtra("resolvedIntent", true) + + val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( + resolvedIntent, + ResolverDataProvider.createResolveInfo(1, 0), + "label", + "extended info", + resolvedIntent, + /* resolveInfoPresentationGetter= */ null) + val chooserTarget = createChooserTarget( + "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id") + val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) + val appTarget = AppTarget( + AppTargetId("id"), + chooserTarget.componentName.packageName, + chooserTarget.componentName.className, + UserHandle.CURRENT) + + val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( + baseDisplayInfo, + mock(), + resolvedIntent, + chooserTarget, + 0.1f, + shortcutInfo, + appTarget, + mock(), + ) + + val refinement = Intent("PROPOSED_REFINEMENT") + assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() + } + + @Test fun testNewDisplayResolveInfo() { val intent = Intent(Intent.ACTION_SEND) intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") @@ -145,6 +205,64 @@ class TargetInfoTest { } @Test + fun test_DisplayResolveInfo_refinementToAlternateSourceIntent() { + val originalIntent = Intent("DONT_REFINE_ME") + originalIntent.putExtra("originalIntent", true) + val mismatchedAlternate = Intent("DOESNT_MATCH") + mismatchedAlternate.putExtra("mismatchedAlternate", true) + val targetAlternate = Intent("REFINE_ME") + targetAlternate.putExtra("targetAlternate", true) + val extraMatch = Intent("REFINE_ME") + extraMatch.putExtra("extraMatch", true) + + val originalInfo = DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + ResolverDataProvider.createResolveInfo(3, 0), + "label", + "extended info", + originalIntent, + /* resolveInfoPresentationGetter= */ null) + originalInfo.addAlternateSourceIntent(mismatchedAlternate) + originalInfo.addAlternateSourceIntent(targetAlternate) + originalInfo.addAlternateSourceIntent(extraMatch) + + val refinement = Intent("REFINE_ME") // First match is `targetAlternate` + refinement.putExtra("refinement", true) + + val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement) + // Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`. + assertThat(refinedResult.resolvedIntent.getBooleanExtra("refinement", false)).isTrue() + assertThat(refinedResult.resolvedIntent.getBooleanExtra("targetAlternate", false)) + .isTrue() + // None of the other source intents got merged in (not even the later one that matched): + assertThat(refinedResult.resolvedIntent.getBooleanExtra("originalIntent", false)) + .isFalse() + assertThat(refinedResult.resolvedIntent.getBooleanExtra("mismatchedAlternate", false)) + .isFalse() + assertThat(refinedResult.resolvedIntent.getBooleanExtra("extraMatch", false)).isFalse() + } + + @Test + fun testDisplayResolveInfo_noSourceIntentMatchingProposedRefinement() { + val originalIntent = Intent("DONT_REFINE_ME") + originalIntent.putExtra("originalIntent", true) + val mismatchedAlternate = Intent("DOESNT_MATCH") + mismatchedAlternate.putExtra("mismatchedAlternate", true) + + val originalInfo = DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + ResolverDataProvider.createResolveInfo(3, 0), + "label", + "extended info", + originalIntent, + /* resolveInfoPresentationGetter= */ null) + originalInfo.addAlternateSourceIntent(mismatchedAlternate) + + val refinement = Intent("PROPOSED_REFINEMENT") + assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull() + } + + @Test fun testNewMultiDisplayResolveInfo() { val intent = Intent(Intent.ACTION_SEND) intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") @@ -186,7 +304,93 @@ class TargetInfoTest { assertThat(multiTargetInfo.hasSelected()).isTrue() assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo) + val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent) + assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java) + assertThat((refined as MultiDisplayResolveInfo).hasSelected()) + .isEqualTo(multiTargetInfo.hasSelected()) + // TODO: consider exercising activity-start behavior. // TODO: consider exercising DisplayResolveInfo base class behavior. } + + @Test + fun testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTarget() { + val sendImage = Intent("SEND").apply { type = "image/png" } + val sendUri = Intent("SEND").apply { type = "text/uri" } + + val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0) + + val imageOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo( + sendImage, + resolveInfo, + "Send Image", + "Sends only images", + sendImage, + /* resolveInfoPresentationGetter= */ null) + + val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo( + sendUri, + resolveInfo, + "Send Text", + "Sends only text", + sendUri, + /* resolveInfoPresentationGetter= */ null) + + val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo( + sendImage, + resolveInfo, + "Send Image or Text", + "Sends images or text", + sendImage, + /* resolveInfoPresentationGetter= */ null + ).apply { + addAlternateSourceIntent(sendUri) + } + + val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( + listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget) + ) + + multiTargetInfo.setSelected(0) + assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget) + assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOnlyTarget.allSourceIntents) + + multiTargetInfo.setSelected(1) + assertThat(multiTargetInfo.selectedTarget).isEqualTo(textOnlyTarget) + assertThat(multiTargetInfo.allSourceIntents).isEqualTo(textOnlyTarget.allSourceIntents) + + multiTargetInfo.setSelected(2) + assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOrTextTarget) + assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOrTextTarget.allSourceIntents) + } + + @Test + fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() { + val refined = Intent("SEND") + val sendImage = Intent("SEND") + val targetOne = spy( + DisplayResolveInfo.newDisplayResolveInfo( + sendImage, + ResolverDataProvider.createResolveInfo(1, 0), + "Target One", + "Target One", + sendImage, + /* resolveInfoPresentationGetter= */ null + ) + ) + val targetTwo = mock<DisplayResolveInfo> { + whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this) + } + + val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( + listOf(targetOne, targetTwo) + ) + + multiTargetInfo.setSelected(1) + assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo) + + multiTargetInfo.tryToCloneWithAppliedRefinement(refined) + verify(targetTwo, times(1)).tryToCloneWithAppliedRefinement(refined) + verify(targetOne, never()).tryToCloneWithAppliedRefinement(any()) + } } diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt new file mode 100644 index 00000000..d870a8c2 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -0,0 +1,203 @@ +/* + * 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.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 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 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) + } + 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 + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + 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 + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) + 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) + 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) + } + ) + } + 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) + 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/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java index 448718cd..006f3b2d 100644 --- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java +++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java @@ -27,7 +27,7 @@ import android.os.Message; import androidx.test.InstrumentationRegistry; -import com.android.intentresolver.ResolverActivity; +import com.android.intentresolver.ResolvedComponentInfo; import org.junit.Test; @@ -37,12 +37,12 @@ public class AbstractResolverComparatorTest { @Test public void testPinned() { - ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + ResolvedComponentInfo r1 = new ResolvedComponentInfo( new ComponentName("package", "class"), new Intent(), new ResolveInfo() ); r1.setPinned(true); - ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + ResolvedComponentInfo r2 = new ResolvedComponentInfo( new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo() ); @@ -60,14 +60,14 @@ public class AbstractResolverComparatorTest { pmInfo1.activityInfo = new ActivityInfo(); pmInfo1.activityInfo.packageName = "aaa"; - ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + ResolvedComponentInfo r1 = new ResolvedComponentInfo( new ComponentName("package", "class"), new Intent(), pmInfo1); r1.setPinned(true); ResolveInfo pmInfo2 = new ResolveInfo(); pmInfo2.activityInfo = new ActivityInfo(); pmInfo2.activityInfo.packageName = "zzz"; - ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + ResolvedComponentInfo r2 = new ResolvedComponentInfo( new ComponentName("zackage", "zlass"), new Intent(), pmInfo2); r2.setPinned(true); @@ -91,7 +91,7 @@ public class AbstractResolverComparatorTest { } @Override - void doCompute(List<ResolverActivity.ResolvedComponentInfo> targets) {} + void doCompute(List<ResolvedComponentInfo> targets) {} @Override public float getScore(ComponentName name) { diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index 5756a0cd..0c817cb2 100644 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -28,6 +28,8 @@ import android.os.UserHandle import android.os.UserManager import androidx.test.filters.SmallTest import com.android.intentresolver.any +import com.android.intentresolver.argumentCaptor +import com.android.intentresolver.capture import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.createAppTarget import com.android.intentresolver.createShareShortcutInfo @@ -39,8 +41,8 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test -import org.mockito.ArgumentCaptor 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 @@ -56,9 +58,15 @@ class ShortcutLoaderTest { private val pm = mock<PackageManager> { whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) } + val userManager = mock<UserManager> { + whenever(isUserRunning(any<UserHandle>())).thenReturn(true) + whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true) + whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false) + } private val context = mock<Context> { whenever(packageManager).thenReturn(pm) whenever(createContextAsUser(any(), anyInt())).thenReturn(this) + whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager) } private val executor = ImmediateExecutor() private val intentFilter = mock<IntentFilter>() @@ -66,7 +74,7 @@ class ShortcutLoaderTest { private val callback = mock<Consumer<ShortcutLoader.Result>>() @Test - fun test_app_predictor_result() { + fun test_queryShortcuts_result_consistency_with_AppPredictor() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -85,24 +93,22 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) - verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) - verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val matchingAppTarget = createAppTarget(matchingShortcutInfo) val shortcuts = listOf( matchingAppTarget, - // mismatching shortcut + // an AppTarget that does not belong to any resolved application; should be ignored createAppTarget( createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) ) ) + val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() + verify(appPredictor, atLeastOnce()) + .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertTrue("An app predictor result is expected", result.isFromAppPredictor) @@ -124,7 +130,7 @@ class ShortcutLoaderTest { } @Test - fun test_shortcut_manager_result() { + fun test_queryShortcuts_result_consistency_with_ShortcutManager() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -153,8 +159,8 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) @@ -175,7 +181,7 @@ class ShortcutLoaderTest { } @Test - fun test_fallback_to_shortcut_manager() { + fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() { val componentName = ComponentName("pkg", "Class") val appTarget = mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) @@ -205,13 +211,13 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) verify(appPredictor, times(1)).requestPredictionUpdate() - val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) + val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() verify(appPredictor, times(1)) - .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor)) appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) - val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) - verify(callback, times(1)).accept(resultCaptor.capture()) + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + verify(callback, times(1)).accept(capture(resultCaptor)) val result = resultCaptor.value assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) @@ -232,32 +238,32 @@ class ShortcutLoaderTest { } @Test - fun test_do_not_call_services_for_not_running_work_profile() { + fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() { testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) } @Test - fun test_do_not_call_services_for_locked_work_profile() { + fun test_queryShortcuts_do_not_call_services_for_locked_work_profile() { testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) } @Test - fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { + fun test_queryShortcuts_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) } @Test - fun test_call_services_for_not_running_main_profile() { + fun test_queryShortcuts_call_services_for_not_running_main_profile() { testAlwaysCallSystemForMainProfile(isUserRunning = false) } @Test - fun test_call_services_for_locked_main_profile() { + fun test_queryShortcuts_call_services_for_locked_main_profile() { testAlwaysCallSystemForMainProfile(isUserUnlocked = false) } @Test - fun test_call_services_if_quite_mode_is_enabled_for_main_profile() { + fun test_queryShortcuts_call_services_if_quite_mode_is_enabled_for_main_profile() { testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) } @@ -267,7 +273,7 @@ class ShortcutLoaderTest { isQuietModeEnabled: Boolean = false ) { val userHandle = UserHandle.of(10) - val userManager = mock<UserManager> { + with(userManager) { whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) @@ -297,7 +303,7 @@ class ShortcutLoaderTest { isQuietModeEnabled: Boolean = false ) { val userHandle = UserHandle.of(10) - val userManager = mock<UserManager> { + with(userManager) { whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) |