diff options
author | 2023-11-21 13:31:12 -0500 | |
---|---|---|
committer | 2023-11-28 15:40:20 -0500 | |
commit | c5d116c2977f5ed83f6994130a3b180deb33f266 (patch) | |
tree | 29a9481348e00c98eaaa60fba4bb9c0c97bf0fde /tests/activity | |
parent | c1c24280a0dfe39745704398ce8d13cf14404eb2 (diff) |
Splits tests
This splits the monolithic 'tests' target into multiple types
based on classification. See the corresponding README.md for
descriptions.
The target 'IntentResolverUnitTests' is replaced by:
IntentResolver-tests-unit
IntentResolver-tests-activity
IntentResolver-tests-integration
IntentResolver-tests-shared
To run everything, use:
atest IntentResolver-tests-*
To run TreeHugger presubmits:
atest --test-mapping pacakges/modules/IntentResolver
Equivalently for postsubmit:
atest --test-mapping pacakges/modules/IntentResolver:postsubmit
Bug: 300157408
Test: atest IntentResolver-tests-*
Change-Id: I9d499284f070a4bfa3e7c1b7c3bbfaa8adb3379b
Diffstat (limited to 'tests/activity')
21 files changed, 11157 insertions, 0 deletions
diff --git a/tests/activity/Android.bp b/tests/activity/Android.bp new file mode 100644 index 00000000..f69caf0e --- /dev/null +++ b/tests/activity/Android.bp @@ -0,0 +1,68 @@ +// +// 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "IntentResolver-tests-activity", + manifest: "AndroidManifest.xml", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "framework", + "framework-res", + ], + + resource_dirs: ["res"], + test_config: "AndroidTest.xml", + static_libs: [ + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.ext.truth", + "androidx.test.espresso.contrib", + "androidx.test.espresso.core", + "androidx.test.rules", + "androidx.test.runner", + "androidx.lifecycle_lifecycle-common-java8", + "androidx.lifecycle_lifecycle-extensions", + "androidx.lifecycle_lifecycle-runtime-testing", + "hilt_android_testing", + "IntentResolver-core", + "IntentResolver-tests-shared", + "junit", + "kotlinx_coroutines_test", + "mockito-target-minus-junit4", + "testables", + "truth", + "truth-java8-extension", + "flag-junit", + "platform-test-annotations", + ], + plugins: ["dagger2-compiler"], + test_suites: ["general-tests"], + sdk_version: "core_platform", + min_sdk_version: "current", + target_sdk_version: "current", + platform_apis: true, +} diff --git a/tests/activity/AndroidManifest.xml b/tests/activity/AndroidManifest.xml new file mode 100644 index 00000000..be05e99e --- /dev/null +++ b/tests/activity/AndroidManifest.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.intentresolver.tests"> + + <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/> + <uses-permission android:name="android.permission.QUERY_USERS"/> + <uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"/> + <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/> + <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" /> + + <application android:name="dagger.hilt.android.testing.HiltTestApplication"> + <uses-library android:name="android.test.runner" /> + <activity android:name="com.android.intentresolver.ChooserWrapperActivity" /> + <activity android:name="com.android.intentresolver.ResolverWrapperActivity" /> + <activity android:name="com.android.intentresolver.v2.ChooserWrapperActivity" /> + <activity android:name="com.android.intentresolver.v2.ResolverWrapperActivity" /> + <provider + android:authorities="com.android.intentresolver.tests" + android:name="com.android.intentresolver.TestContentProvider" + android:grantUriPermissions="true" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.intentresolver.tests"> + </instrumentation> + +</manifest> diff --git a/tests/activity/AndroidTest.xml b/tests/activity/AndroidTest.xml new file mode 100644 index 00000000..6c9d4953 --- /dev/null +++ b/tests/activity/AndroidTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2021 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. +--> +<configuration description="Run IntentResolver Tests."> + <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="IntentResolver-tests-activity.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="input keyevent KEYCODE_WAKEUP" /> + <option name="run-command" value="wm dismiss-keyguard" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.intentresolver.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/tests/activity/res/drawable/test320x240.png b/tests/activity/res/drawable/test320x240.png Binary files differnew file mode 100644 index 00000000..9b5800da --- /dev/null +++ b/tests/activity/res/drawable/test320x240.png diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java new file mode 100644 index 00000000..3ee80c14 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2021 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.UserHandle; + +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.shortcuts.ShortcutLoader; + +import java.util.function.Consumer; +import java.util.function.Function; + +import kotlin.jvm.functions.Function2; + +/** + * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing. + * We cannot directly mock the activity created since instrumentation creates it, so instead we use + * this singleton to modify behavior. + */ +public class ChooserActivityOverrideData { + private static ChooserActivityOverrideData sInstance = null; + + public static ChooserActivityOverrideData getInstance() { + if (sInstance == null) { + sInstance = new ChooserActivityOverrideData(); + } + return sInstance; + } + + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<TargetInfo, Boolean> onSafelyStartInternalCallback; + public Function<TargetInfo, Boolean> onSafelyStartCallback; + public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> + shortcutLoaderFactory = (userHandle, callback) -> null; + public ChooserActivity.ChooserListController resolverListController; + public ChooserActivity.ChooserListController workResolverListController; + public Boolean isVoiceInteraction; + public Cursor resolverCursor; + public boolean resolverForceException; + public ImageLoader imageLoader; + public int alternateProfileSetting; + public Resources resources; + public AnnotatedUserHandles annotatedUserHandles; + public boolean hasCrossProfileIntents; + public boolean isQuietModeEnabled; + public Integer myUserId; + public WorkProfileAvailabilityManager mWorkProfileAvailability; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; + public PackageManager packageManager; + + public void reset() { + onSafelyStartInternalCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + imageLoader = null; + resolverCursor = null; + resolverForceException = false; + resolverListController = mock(ChooserActivity.ChooserListController.class); + workResolverListController = mock(ChooserActivity.ChooserListController.class); + alternateProfileSetting = 0; + resources = null; + annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM) + .setPersonalProfileUserHandle(UserHandle.SYSTEM) + .build(); + hasCrossProfileIntents = true; + isQuietModeEnabled = false; + myUserId = null; + packageManager = null; + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { + @Override + public boolean isQuietModeEnabled() { + return isQuietModeEnabled; + } + + @Override + public boolean isWorkProfileUserUnlocked() { + return true; + } + + @Override + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } + }; + shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); + } + + private ChooserActivityOverrideData() {} +} + diff --git a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java new file mode 100644 index 00000000..72f1f452 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2008 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.prediction.AppPredictor; +import android.app.usage.UsageStatsManager; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModelProvider; + +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.TargetDataLoader; +import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Simple wrapper around chooser activity to be able to initiate it under test. For more + * information, see {@code com.android.internal.app.ChooserWrapperActivity}. + */ +public class ChooserWrapperActivity extends ChooserActivity implements IChooserWrapper { + static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance(); + private UsageStatsManager mUsm; + + // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at + // onCreate and needs to see some non-negative value in the test. + @Override + public int getLaunchedFromUid() { + return 1234; + } + + @Override + public ChooserListAdapter createChooserListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + UserHandle userHandle, + Intent targetIntent, + ChooserRequestParameters chooserRequest, + int maxTargetsPerRow, + TargetDataLoader targetDataLoader) { + PackageManager packageManager = + sOverrides.packageManager == null ? context.getPackageManager() + : sOverrides.packageManager; + return new ChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + userHandle, + targetIntent, + this, + packageManager, + getEventLog(), + chooserRequest, + maxTargetsPerRow, + userHandle, + targetDataLoader); + } + + @Override + public ChooserListAdapter getAdapter() { + return mChooserMultiProfilePagerAdapter.getActiveListAdapter(); + } + + @Override + public ChooserListAdapter getPersonalListAdapter() { + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)) + .getListAdapter(); + } + + @Override + public ChooserListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)) + .getListAdapter(); + } + + @Override + public boolean getIsSelected() { + return mIsSuccessfullySelected; + } + + @Override + 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 + public UsageStatsManager getUsageStatsManager() { + if (mUsm == null) { + mUsm = getSystemService(UsageStatsManager.class); + } + return mUsm; + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { + if (sOverrides.mWorkProfileAvailability != null) { + return sOverrides.mWorkProfileAvailability; + } + return super.createWorkProfileAvailabilityManager(); + } + + @Override + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(cti)) { + return; + } + super.safelyStartActivityInternal(cti, user, options); + } + + @Override + protected ChooserListController createListController(UserHandle userHandle) { + if (userHandle == UserHandle.SYSTEM) { + return sOverrides.resolverListController; + } + return sOverrides.workResolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + @Override + public Resources getResources() { + if (sOverrides.resources != null) { + return sOverrides.resources; + } + return super.getResources(); + } + + @Override + protected ViewModelProvider.Factory createPreviewViewModelFactory() { + return TestContentPreviewViewModel.Companion.wrap( + super.createPreviewViewModelFactory(), + sOverrides.imageLoader); + } + + @Override + public Cursor queryResolver(ContentResolver resolver, Uri uri) { + if (sOverrides.resolverCursor != null) { + return sOverrides.resolverCursor; + } + + if (sOverrides.resolverForceException) { + throw new SecurityException("Test exception handling"); + } + + return super.queryResolver(resolver, uri); + } + + @Override + protected boolean isWorkProfile() { + if (sOverrides.alternateProfileSetting != 0) { + return sOverrides.alternateProfileSetting == MetricsEvent.MANAGED_PROFILE; + } + return super.isWorkProfile(); + } + + @Override + public DisplayResolveInfo createTestDisplayResolveInfo( + Intent originalIntent, + ResolveInfo pri, + CharSequence pLabel, + CharSequence pInfo, + Intent replacementIntent) { + return DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + pri, + pLabel, + pInfo, + replacementIntent); + } + + @Override + protected AnnotatedUserHandles computeAnnotatedUserHandles() { + return sOverrides.annotatedUserHandles; + } + + @Override + public UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @NonNull + @Override + public Context createContextAsUser(UserHandle user, int flags) { + // return the current context as a work profile doesn't really exist in these tests + return this; + } + + @Override + protected ShortcutLoader createShortcutLoader( + Context context, + AppPredictor appPredictor, + UserHandle userHandle, + IntentFilter targetIntentFilter, + Consumer<ShortcutLoader.Result> callback) { + ShortcutLoader shortcutLoader = + sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); + if (shortcutLoader != null) { + return shortcutLoader; + } + return super.createShortcutLoader( + context, appPredictor, userHandle, targetIntentFilter, callback); + } +} diff --git a/tests/activity/src/com/android/intentresolver/IChooserWrapper.java b/tests/activity/src/com/android/intentresolver/IChooserWrapper.java new file mode 100644 index 00000000..481cf3b2 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/IChooserWrapper.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 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.usage.UsageStatsManager; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; + +import androidx.annotation.Nullable; + +import com.android.intentresolver.chooser.DisplayResolveInfo; + +import java.util.concurrent.Executor; + +/** + * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in + * order to expose the internals for override/inspection. Implementations should apply the overrides + * specified by the {@code ChooserActivityOverrideData} singleton. + */ +public interface IChooserWrapper { + ChooserListAdapter getAdapter(); + ChooserListAdapter getPersonalListAdapter(); + ChooserListAdapter getWorkListAdapter(); + boolean getIsSelected(); + UsageStatsManager getUsageStatsManager(); + DisplayResolveInfo createTestDisplayResolveInfo( + Intent originalIntent, + ResolveInfo pri, + CharSequence pLabel, + CharSequence pInfo, + @Nullable Intent replacementIntent); + UserHandle getCurrentUserHandle(); + Executor getMainExecutor(); +} diff --git a/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java new file mode 100644 index 00000000..dde2f980 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java @@ -0,0 +1,1097 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.MatcherUtils.first; +import static com.android.intentresolver.ResolverWrapperActivity.sOverrides; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.intentresolver.widget.ResolverDrawerLayout; + +import com.google.android.collect.Lists; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +/** + * Resolver activity instrumentation tests + */ +@RunWith(AndroidJUnit4.class) +public class ResolverActivityTest { + + private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app + .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10); + private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11); + + @Rule + public ActivityTestRule<ResolverWrapperActivity> mActivityRule = + new ActivityTestRule<>(ResolverWrapperActivity.class, false, false); + + @Before + public void setup() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Ignore // Failing - b/144929805 + @Test + public void setMaxHeight() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final int initialResolverHeight = viewPager.getHeight(); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight - 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should be capped at maxHeight", + viewPager.getHeight() == (initialResolverHeight - 1)); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight + 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should not change height if its height is less than maxHeight", + viewPager.getHeight() == initialResolverHeight); + } + + @Ignore // Failing - b/144929805 + @Test + public void setShowAtTopToTrue() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final View divider = activity.findViewById(com.android.internal.R.id.divider); + final RelativeLayout profileView = + (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button) + .getParent(); + assertThat("Drawer should show at bottom by default", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() > 0); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + layout.setShowAtTop(true); + }); + waitForIdle(); + assertThat("Drawer should show at top with new attribute", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() == 0); + } + + @Test + public void hasLastChosenActivity() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileOneOption() throws Exception { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + + ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); + Intent sendIntent = createSendImageIntent(); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10, + PERSONAL_USER_HANDLE); + // We pick the first one as there is another one in the work profile side + onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + + @Test + public void hasLastChosenActivityAndOtherProfile() throws Exception { + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + Intent sendIntent = createSendImageIntent(); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + Intent sendIntent = createSendImageIntent(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, + new ArrayList<>(workResolvedComponentInfos)); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + // The work list adapter must be populated in advance before tapping the other tab + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_workTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_personalTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(2)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets() + throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_headerIsVisibleInPersonalTab() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); + String initialText = headerText.getText().toString(); + assertFalse("Header text is empty.", initialText.isEmpty()); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + } + + @Test + public void testWorkTab_switchTabs_headerStaysSame() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); + String initialText = headerText.getText().toString(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + String currentText = headerText.getText().toString(); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + assertThat(String.format("Header text is not the same when switching tabs, personal profile" + + " header was %s but work profile header is %s", initialText, currentText), + TextUtils.equals(initialText, currentText)); + } + + @Test + public void testWorkTab_noPersonalApps_canStartWorkApps() + throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf( + withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), + isDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_workProfileDisabled_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.isQuietModeEnabled = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_turn_on_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + sOverrides.hasCrossProfileIntents = false; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE); + // Personal profile only has a browser + personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver_noCurrentProfileTarget() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Need to ensure mini resolver doesn't trigger here. + assertNotMiniResolver(); + } + + private void assertNotMiniResolver() { + try { + onView(withId(com.android.internal.R.id.open_cross_profile)) + .check(matches(isDisplayed())); + } catch (NoMatchingViewException e) { + return; + } + fail("Mini resolver present but shouldn't be"); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertNull(chosen[0]); + } + + @Test + public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 2, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + + onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled())); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + } + + @Test + public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(first(allOf(withText(personalResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + List<UserHandle> result = activity + .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE); + + assertThat(result.containsAll( + Lists.newArrayList(PERSONAL_USER_HANDLE, CLONE_PROFILE_USER_HANDLE)), is(true)); + } + + private Intent createSendImageIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("image/jpeg"); + return sendIntent; + } + + private Intent createOpenWebsiteIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_VIEW); + sendIntent.setData(Uri.parse("https://google.com")); + return sendIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + resolvedForUser)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId, UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + resolvedForUser)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + } + return infoList; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) { + AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder(); + handles + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE); + if (workAvailable) { + handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE); + } + if (cloneAvailable) { + handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE); + } + sOverrides.annotatedUserHandles = handles.build(); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .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/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java new file mode 100644 index 00000000..d1adfba9 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -0,0 +1,285 @@ +/* + * Copyright (C) 2017 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 static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.espresso.idling.CountingIdlingResource; + +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.SelectableTargetInfo; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.icons.LabelInfo; +import com.android.intentresolver.icons.TargetDataLoader; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/* + * Simple wrapper around chooser activity to be able to initiate it under test + */ +public class ResolverWrapperActivity extends ResolverActivity { + static final OverrideData sOverrides = new OverrideData(); + + private final CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); + + public ResolverWrapperActivity() { + super(/* isIntentPicker= */ true); + } + + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + + @Override + public ResolverListAdapter createResolverListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle, + TargetDataLoader targetDataLoader) { + return new ResolverListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + userHandle, + payloadIntents.get(0), // TODO: extract upstream + this, + userHandle, + new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource)); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { + if (sOverrides.mWorkProfileAvailability != null) { + return sOverrides.mWorkProfileAvailability; + } + return super.createWorkProfileAvailabilityManager(); + } + + ResolverListAdapter getAdapter() { + return mMultiProfilePagerAdapter.getActiveListAdapter(); + } + + ResolverListAdapter getPersonalListAdapter() { + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)); + } + + ResolverListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)); + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) { + return; + } + super.safelyStartActivityInternal(cti, user, options); + } + + @Override + protected ResolverListController createListController(UserHandle userHandle) { + if (userHandle == UserHandle.SYSTEM) { + return sOverrides.resolverListController; + } + return sOverrides.workResolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + protected UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @Override + protected AnnotatedUserHandles computeAnnotatedUserHandles() { + return sOverrides.annotatedUserHandles; + } + @Override + public void startActivityAsUser( + @NonNull Intent intent, + Bundle options, + @NonNull UserHandle user + ) { + super.startActivityAsUser(intent, options, user); + } + + @Override + protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle + userHandle) { + return super.getResolverRankerServiceUserHandleListInternal(userHandle); + } + + /** + * We cannot directly mock the activity created since instrumentation creates it. + * <p> + * Instead, we use static instances of this object to modify behavior. + */ + static class OverrideData { + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback; + public ResolverListController resolverListController; + public ResolverListController workResolverListController; + public Boolean isVoiceInteraction; + public AnnotatedUserHandles annotatedUserHandles; + public Integer myUserId; + public boolean hasCrossProfileIntents; + public boolean isQuietModeEnabled; + public WorkProfileAvailabilityManager mWorkProfileAvailability; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; + + public void reset() { + onSafelyStartInternalCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + resolverListController = mock(ResolverListController.class); + workResolverListController = mock(ResolverListController.class); + annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM) + .setPersonalProfileUserHandle(UserHandle.SYSTEM) + .build(); + myUserId = null; + hasCrossProfileIntents = true; + isQuietModeEnabled = false; + + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { + @Override + public boolean isQuietModeEnabled() { + return isQuietModeEnabled; + } + + @Override + public boolean isWorkProfileUserUnlocked() { + return true; + } + + @Override + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); + } + } + + private static class TargetDataLoaderWrapper extends TargetDataLoader { + private final TargetDataLoader mTargetDataLoader; + private final CountingIdlingResource mLabelIdlingResource; + + private TargetDataLoaderWrapper( + TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) { + mTargetDataLoader = targetDataLoader; + mLabelIdlingResource = labelIdlingResource; + } + + @Override + public void loadAppTargetIcon( + @NonNull DisplayResolveInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); + } + + @Override + public void loadDirectShareIcon( + @NonNull SelectableTargetInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback); + } + + @Override + public void loadLabel( + @NonNull DisplayResolveInfo info, + @NonNull Consumer<LabelInfo> callback) { + mLabelIdlingResource.increment(); + mTargetDataLoader.loadLabel( + info, + (result) -> { + mLabelIdlingResource.decrement(); + callback.accept(result); + }); + } + + @Override + public void getOrLoadLabel(@NonNull DisplayResolveInfo info) { + mTargetDataLoader.getOrLoadLabel(info); + } + } +} diff --git a/tests/activity/src/com/android/intentresolver/TestContentProvider.kt b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt new file mode 100644 index 00000000..426f9af2 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri + +class TestContentProvider : ContentProvider() { + override fun query( + uri: Uri, + projection: Array<out String>?, + selection: String?, + selectionArgs: Array<out String>?, + sortOrder: String? + ): Cursor? = null + + override fun getType(uri: Uri): String? = + runCatching { uri.getQueryParameter(PARAM_MIME_TYPE) }.getOrNull() + + override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? { + val delay = + runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE_TIMEOUT)?.toLong() ?: 0L } + .getOrDefault(0L) + if (delay > 0) { + try { + Thread.sleep(delay) + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } + } + return runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE)?.let { arrayOf(it) } } + .getOrNull() + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? = null + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = 0 + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array<out String>? + ): Int = 0 + + override fun onCreate(): Boolean = true + + companion object { + const val PARAM_MIME_TYPE = "mimeType" + const val PARAM_STREAM_TYPE = "streamType" + const val PARAM_STREAM_TYPE_TIMEOUT = "streamTypeTo" + } +} diff --git a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java new file mode 100644 index 00000000..f597d7f2 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -0,0 +1,3127 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver; + +import 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; +import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; +import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; +import static com.android.intentresolver.MatcherUtils.first; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static junit.framework.Assert.assertNull; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +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; +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; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.DeviceConfig; +import android.service.chooser.ChooserAction; +import android.service.chooser.ChooserTarget; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.Pair; +import android.util.SparseArray; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.contrib.RecyclerViewActions; +import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; +import androidx.test.espresso.matcher.ViewMatchers; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; + +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.logging.FakeEventLog; +import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import dagger.hilt.android.testing.HiltAndroidRule; +import dagger.hilt.android.testing.HiltAndroidTest; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Instrumentation tests for ChooserActivity. + * <p> + * Legacy test suite migrated from framework CoreTests. + * <p> + */ +@RunWith(Parameterized.class) +@HiltAndroidTest +public class UnbundledChooserActivityTest { + + private static FakeEventLog getEventLog(ChooserWrapperActivity activity) { + return (FakeEventLog) activity.mEventLog; + } + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10); + private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11); + + private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm; + private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM = + pm -> { + PackageManager mock = Mockito.spy(pm); + when(mock.getAppPredictionServicePackageName()).thenReturn(null); + return mock; + }; + + @Parameterized.Parameters + public static Collection packageManagers() { + return Arrays.asList(new Object[][] { + // Default PackageManager + { DEFAULT_PM }, + // No App Prediction Service + { NO_APP_PREDICTION_SERVICE_PM} + }); + } + + private static final String TEST_MIME_TYPE = "application/TestType"; + + private static final int CONTENT_PREVIEW_IMAGE = 1; + private static final int CONTENT_PREVIEW_FILE = 2; + private static final int CONTENT_PREVIEW_TEXT = 3; + + @Rule(order = 0) + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule(order = 1) + public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this); + + @Rule(order = 2) + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, false); + + @Before + public void setUp() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + cleanOverrideData(); + mHiltAndroidRule.inject(); + } + + private final Function<PackageManager, PackageManager> mPackageManagerOverride; + + public UnbundledChooserActivityTest( + Function<PackageManager, PackageManager> packageManagerOverride) { + mPackageManagerOverride = packageManagerOverride; + } + + private void setDeviceConfigProperty( + @NonNull String propertyName, + @NonNull String value) { + // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly + // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently + // configure in {@link #setup()}. + // TODO: is it really appropriate that this is always set with makeDefault=true? + boolean valueWasSet = DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SYSTEMUI, + propertyName, + value, + true /* makeDefault */); + if (!valueWasSet) { + throw new IllegalStateException( + "Could not set " + propertyName + " to " + value); + } + } + + public void cleanOverrideData() { + ChooserActivityOverrideData.getInstance().reset(); + ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride; + + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(true)); + } + + @Test + public void customTitle() throws InterruptedException { + Intent viewIntent = createViewTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity( + Intent.createChooser(viewIntent, "chooser test")); + + waitForIdle(); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getServiceTargetCount(), is(0)); + onView(withId(android.R.id.title)).check(matches(withText("chooser test"))); + } + + @Test + public void customTitleIgnoredForSendIntents() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); + waitForIdle(); + onView(withId(android.R.id.title)) + .check(matches(withText(R.string.whichSendApplication))); + } + + @Test + public void emptyTitle() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(android.R.id.title)) + .check(matches(withText(R.string.whichSendApplication))); + } + + @Test + public void test_shareRichTextWithRichTitle_richTextAndRichTitleDisplayed() { + CharSequence title = new SpannableStringBuilder() + .append("Rich", new UnderlineSpan(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + "Title", + new ForegroundColorSpan(Color.RED), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + CharSequence sharedText = new SpannableStringBuilder() + .append( + "Rich", + new BackgroundColorSpan(Color.YELLOW), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + "Text", + new StyleSpan(Typeface.ITALIC), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + Intent sendIntent = createSendTextIntent(); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + sendIntent.putExtra(Intent.EXTRA_TITLE, title); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.content_preview_title)) + .check((view, e) -> { + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + assertThat(spanned.getSpans(0, spanned.length(), Object.class)) + .hasLength(2); + assertThat(spanned.getSpans(0, 4, UnderlineSpan.class)).hasLength(1); + assertThat(spanned.getSpans(4, spanned.length(), ForegroundColorSpan.class)) + .hasLength(1); + }); + + onView(withId(com.android.internal.R.id.content_preview_text)) + .check((view, e) -> { + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + assertThat(spanned.getSpans(0, spanned.length(), Object.class)) + .hasLength(2); + assertThat(spanned.getSpans(0, 4, BackgroundColorSpan.class)).hasLength(1); + assertThat(spanned.getSpans(4, spanned.length(), StyleSpan.class)).hasLength(1); + }); + } + + @Test + public void emptyPreviewTitleAndThumbnail() throws InterruptedException { + Intent sendIntent = createSendTextIntentWithPreview(null, null); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(not(isDisplayed()))); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleWithoutThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(withText(previewTitle))); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleWithInvalidThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, + Uri.parse("tel:(+49)12345789")); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleAndThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + com.android.intentresolver.tests.R.drawable.test320x240); + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(isDisplayed())); + } + + @Test @Ignore + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void fourOptionsStackedIntoOneTarget() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + + // create just enough targets to ensure the a-z list should be shown + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); + + // next create 4 targets in a single app that should be stacked into a single target + String packageName = "xxx.yyy"; + String appName = "aaa"; + ComponentName cn = new ComponentName(packageName, appName); + Intent intent = new Intent("fakeIntent"); + List<ResolvedComponentInfo> infosToStack = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i, + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); + resolveInfo.activityInfo.applicationInfo.name = appName; + resolveInfo.activityInfo.applicationInfo.packageName = packageName; + resolveInfo.activityInfo.packageName = packageName; + resolveInfo.activityInfo.name = "ccc" + i; + infosToStack.add(new ResolvedComponentInfo(cn, intent, resolveInfo)); + } + resolvedComponentInfos.addAll(infosToStack); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // expect 1 unique targets + 1 group + 4 ranked app targets + assertThat(activity.getAdapter().getCount(), is(6)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + onView(allOf(withText(appName), hasSibling(withText("")))).perform(click()); + waitForIdle(); + + // clicking will launch a dialog to choose the activity within the app + onView(withText(appName)).check(matches(isDisplayed())); + int i = 0; + for (ResolvedComponentInfo rci: infosToStack) { + onView(withText("ccc" + i)).check(matches(isDisplayed())); + ++i; + } + } + + @Test @Ignore + public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + UsageStatsManager usm = activity.getUsageStatsManager(); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .topK(any(List.class), anyInt()); + assertThat(activity.getIsSelected(), is(false)); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + return true; + }; + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo( + sendIntent, toChoose, "testLabel", "testInfo", sendIntent); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .updateChooserCounts(Mockito.anyString(), any(UserHandle.class), + Mockito.anyString()); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .updateModel(testDri); + assertThat(activity.getIsSelected(), is(true)); + } + + @Ignore // b/148158199 + @Test + public void noResultsFromPackageManager() { + setupResolverControllers(null); + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper wrapper = (IChooserWrapper) activity; + + waitForIdle(); + assertThat(activity.isFinishing(), is(false)); + + onView(withId(android.R.id.empty)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed()))); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> wrapper.getAdapter().handlePackagesChanged() + ); + // backward compatibility. looks like we finish when data is empty after package change + assertThat(activity.isFinishing(), is(true)); + } + + @Test + public void autoLaunchSingleResult() throws InterruptedException { + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); + setupResolverControllers(resolvedComponentInfos); + + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat(activity.isFinishing(), is(true)); + } + + @Test @Ignore + public void hasOtherProfileOneOption() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); + Intent sendIntent = createSendTextIntent(); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); + waitForIdle(); + + onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void hasLastChosenActivityAndOtherProfile() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + @Ignore("b/285309527") + public void testFilePlusTextSharing_ExcludeText() { + Uri uri = createTestContentProviderUri(null, "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testFilePlusTextSharing_RemoveAndAddBackText() { + Uri uri = createTestContentProviderUri("application/pdf", "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + final String text = "https://google.com/search?q=google"; + sendIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + + onView(withId(R.id.include_text_action)) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.content_preview_text)).check(matches(withText(text))); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + Intent alternativeIntent = createSendTextIntent(); + final String text = "alternative intent"; + alternativeIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + alternativeIntent, PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.image_view)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + onView(withId(R.id.content_preview_text)) + .check(matches(allOf(isDisplayed(), withText("Image only")))); + } + + @Test + public void copyTextToClipboard() { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService( + Context.CLIPBOARD_SERVICE); + ClipData clipData = clipboard.getPrimaryClip(); + assertThat(clipData).isNotNull(); + assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending"); + + ClipDescription clipDescription = clipData.getDescription(); + assertThat("text/plain", is(clipDescription.getMimeType(0))); + + assertEquals(mActivityRule.getActivityResult().getResultCode(), RESULT_OK); + } + + @Test + public void copyTextToClipboardLogging() { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionSelected()) + .isEqualTo(new FakeEventLog.ActionSelected( + /* targetType = */ EventLog.SELECTION_TYPE_COPY)); + } + + @Test + @Ignore + public void testNearbyShareLogging() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_nearby_button)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + + + @Test @Ignore + public void testEditImageLogs() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + + @Test + public void oneVisibleImagePreview() { + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createWideBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(1)); + assertThat(recyclerView.getChildCount(), is(1)); + View imageView = recyclerView.getChildAt(0); + Rect rect = new Rect(); + boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect); + assertThat( + "image preview view is not fully visible", + isPartiallyVisible + && rect.width() == imageView.getWidth() + && rect.height() == imageView.getHeight()); + }); + } + + @Test + public void allThumbnailsFailedToLoad_hidePreview() { + Uri uri = createTestContentProviderUri("image/jpg", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + } + + @Test(timeout = 4_000) + public void testSlowUriMetadata_fallbackToFilePreview() { + Uri uri = createTestContentProviderUri( + "application/pdf", "image/png", /*streamTypeTimeout=*/8_000); + ArrayList<Uri> uris = new ArrayList<>(1); + uris.add(uri); + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + // The preview type resolution is expected to timeout and default to file preview, otherwise + // the test should timeout. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test(timeout = 4_000) + public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi() { + Uri fileUri = createTestContentProviderUri( + "application/pdf", "application/pdf", /*streamTypeTimeout=*/300); + Uri imageUri = createTestContentProviderUri("application/pdf", "image/png"); + ArrayList<Uri> uris = new ArrayList<>(50); + for (int i = 0; i < 49; i++) { + uris.add(fileUri); + } + uris.add(imageUri); + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(imageUri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + // The preview type resolution is expected to timeout and default to file preview, otherwise + // the test should timeout. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + waitForIdle(); + + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void testManyVisibleImagePreview_ScrollableImagePreview() { + Uri uri = createTestContentProviderUri("image/png", null); + + 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().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .perform(RecyclerViewActions.scrollToLastPosition()) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size())); + }); + } + + @Test(timeout = 4_000) + public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart() { + Uri imgOneUri = createTestContentProviderUri("image/png", null); + Uri imgTwoUri = createTestContentProviderUri("image/png", null) + .buildUpon() + .path("image-2.png") + .build(); + Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 8_000); + ArrayList<Uri> uris = new ArrayList<>(2); + // two large previews to fill the screen and be presented right away and one + // document that would be delayed by the URI metadata reading + uris.add(imgOneUri); + uris.add(imgTwoUri); + uris.add(docUri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + Map<Uri, Bitmap> bitmaps = new HashMap<>(); + bitmaps.put(imgOneUri, createWideBitmap(Color.RED)); + bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN)); + bitmaps.put(docUri, createWideBitmap(Color.BLUE)); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(bitmaps); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + // the preview type is expected to be resolved quickly based on the first provided URI + // metadata. If, instead, it is dependent on the third URI metadata, the test should either + // timeout or (more probably due to inner timeout) default to file preview type; anyway the + // test will fail. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.scrollable_image_preview)) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getChildCount()).isAtLeast(1); + // the first view is a preview + View imageView = recyclerView.getChildAt(0).findViewById(R.id.image); + assertThat(imageView).isNotNull(); + }) + .perform(RecyclerViewActions.scrollToLastPosition()) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getChildCount()).isAtLeast(1); + // check that the last view is a loading indicator + View loadingIndicator = + recyclerView.getChildAt(recyclerView.getChildCount() - 1); + assertThat(loadingIndicator).isNotNull(); + }); + waitForIdle(); + } + + @Test + public void testImageAndTextPreview() { + final Uri uri = createTestContentProviderUri("image/png", null); + 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().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText)) + .check(matches(isDisplayed())); + } + + @Test + public void test_shareImageWithRichText_RichTextIsDisplayed() { + final Uri uri = createTestContentProviderUri("image/png", null); + final CharSequence sharedText = new SpannableStringBuilder() + .append( + "text-", + new StyleSpan(Typeface.BOLD_ITALIC), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + Long.toString(System.currentTimeMillis()), + new ForegroundColorSpan(Color.RED), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText.toString())) + .check(matches(isDisplayed())) + .check((view, e) -> { + if (e != null) { + throw e; + } + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + Object[] spans = spanned.getSpans(0, text.length(), Object.class); + assertThat(spans).hasLength(2); + assertThat(spanned.getSpans(0, 5, StyleSpan.class)).hasLength(1); + assertThat(spanned.getSpans(5, text.length(), ForegroundColorSpan.class)) + .hasLength(1); + }); + } + + @Test + public void testTextPreviewWhenTextIsSharedWithMultipleImages() { + final Uri uri = createTestContentProviderUri("image/png", null); + final String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + Mockito.any(UserHandle.class))) + .thenReturn(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText)).check(matches(isDisplayed())); + } + + @Test + public void testOnCreateLogging() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isFalse(); + assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE); + } + + @Test + public void testOnCreateLoggingFromWorkProfile() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ChooserActivityOverrideData.getInstance().alternateProfileSetting = + MetricsEvent.MANAGED_PROFILE; + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isTrue(); + assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE); + } + + @Test + public void testEmptyPreviewLogging() { + Intent sendIntent = createSendTextIntentWithPreview(null, null); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, + "empty preview logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isFalse(); + assertThat(event.getTargetMimeType()).isNull(); + } + + @Test + public void testTitlePreviewLogging() { + Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionShareWithPreview()) + .isEqualTo(new FakeEventLog.ActionShareWithPreview( + /* previewType = */ CONTENT_PREVIEW_TEXT)); + } + + @Test + public void testImagePreviewLogging() { + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionShareWithPreview()) + .isEqualTo(new FakeEventLog.ActionShareWithPreview( + /* previewType = */ CONTENT_PREVIEW_IMAGE)); + } + + @Test + public void oneVisibleFilePreview() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + + @Test + public void moreThanOneVisibleFilePreview() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 2 more files"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void contentProviderThrowSecurityException() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + ChooserActivityOverrideData.getInstance().resolverForceException = true; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void contentProviderReturnsNoColumns() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + Cursor cursor = mock(Cursor.class); + when(cursor.getCount()).thenReturn(1); + Mockito.doNothing().when(cursor).close(); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndex(Mockito.anyString())).thenReturn(-1); + + ChooserActivityOverrideData.getInstance().resolverCursor = cursor; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 1 more file"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void testGetBaseScore() { + final float testBaseScore = 0.89f; + + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getScore(Mockito.isA(DisplayResolveInfo.class))) + .thenReturn(testBaseScore); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + final DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo( + sendIntent, + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), + "testLabel", + "testInfo", + sendIntent); + final ChooserListAdapter adapter = activity.getAdapter(); + + assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_CHOOSER_TARGET), is(testBaseScore)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE), + is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER), + is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test + public void testDirectTargetSelectionLogging() { + 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 = + createShortcutLoaderFactory(); + + // Start activity + ChooserWrapperActivity activity = + 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(-1); + var hashResult = call.getDirectTargetHashed(); + var hash = hashResult == null ? "" : hashResult.hashedString; + assertWithMessage("Hash is not predictable but must be obfuscated") + .that(hash).isNotEqualTo(name); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test + public void testDirectTargetLoggingWithRankedAppTarget() { + 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 = + createShortcutLoaderFactory(); + + // Start activity + ChooserWrapperActivity activity = + 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(0); + } + + @Test + public void testShortcutTargetWithApplyAppLimits() { + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + 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 = + createShortcutLoaderFactory(); + + // 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); + } + + @Test + public void testShortcutTargetWithoutApplyAppLimits() { + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(false)); + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + 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 = + createShortcutLoaderFactory(); + + // 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 4 targets (2 apps, 2 direct)", + activeAdapter.getCount(), + is(4)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(2)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); + assertThat( + "The display label must match", + activeAdapter.getItem(1).getDisplayLabel(), + is("testTitle1")); + } + + @Test + public void testLaunchWithCallerProvidedTarget() { + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(false)); + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + // set caller-provided target + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + String callerTargetLabel = "Caller Target"; + ChooserTarget[] targets = new ChooserTarget[] { + new ChooserTarget( + callerTargetLabel, + Icon.createWithBitmap(createBitmap()), + 0.1f, + resolvedComponentInfos.get(0).name, + new Bundle()) + }; + chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets); + + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); + + // Start activity + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[0], + new HashMap<>(), + new HashMap<>()); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is(callerTargetLabel)); + + // Switch to work profile and ensure that the target *doesn't* show up there. + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + for (int i = 0; i < activity.getWorkListAdapter().getCount(); i++) { + assertThat( + "Chooser target should not show up in opposite profile", + activity.getWorkListAdapter().getItem(i).getDisplayLabel(), + not(callerTargetLabel)); + } + } + + @Test + 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), + Context.RECEIVER_EXPORTED); + + try { + onView(withText(customActionLabel)).perform(click()); + assertTrue("Timeout waiting for broadcast", + broadcastInvoked.await(5000, TimeUnit.MILLISECONDS)); + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test + 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); + String label = "modify share"; + PendingIntent pendingIntent = PendingIntent.getBroadcast( + testContext, + 123, + new Intent(modifyShareAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap( + createBitmap()), label, pendingIntent).build(); + chooserIntent.putExtra( + Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, + action); + // 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), + Context.RECEIVER_EXPORTED); + + try { + onView(withText(label)).perform(click()); + assertTrue("Timeout waiting for broadcast", + broadcastInvoked.await(5000, TimeUnit.MILLISECONDS)); + + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test + public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException { + updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4); + givenAppTargets(/* appCount= */ 16); + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + updateMaxTargetsPerRowResource(/* targetsPerRow= */ 6); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(() -> activity.onConfigurationChanged( + InstrumentationRegistry.getInstrumentation() + .getContext().getResources().getConfiguration())); + + waitForIdle(); + onView(withId(com.android.internal.R.id.resolver_list)) + .check(matches(withGridColumnCount(6))); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test @Ignore + public void testDirectTargetLoggingWithAppTargetNotRankedPortrait() + throws InterruptedException { + testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_PORTRAIT, 4); + } + + @Test @Ignore + public void testDirectTargetLoggingWithAppTargetNotRankedLandscape() + throws InterruptedException { + testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_LANDSCAPE, 8); + } + + private void testDirectTargetLoggingWithAppTargetNotRanked( + int orientation, int appTargetsExpected) { + Configuration configuration = + new Configuration(InstrumentationRegistry.getInstrumentation().getContext() + .getResources().getConfiguration()); + configuration.orientation = orientation; + + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(configuration).when(resources).getConfiguration(); + + Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15); + setupResolverControllers(resolvedComponentInfos); + + // Create direct share target + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE); + + // Start activity + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + // Insert the direct share target + Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); + directShareToShortcutInfos.put(serviceTargets.get(0), null); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activity.getAdapter().addServiceResults( + activity.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent), + serviceTargets, + TARGET_TYPE_CHOOSER_TARGET, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) + ); + + assertThat( + String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)", + appTargetsExpected + 16, appTargetsExpected), + activity.getAdapter().getCount(), is(appTargetsExpected + 16)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + var invocations = eventLog.getShareTargetSelected(); + assertWithMessage("Only one ShareTargetSelected event logged") + .that(invocations).hasSize(1); + FakeEventLog.ShareTargetSelected call = invocations.get(0); + assertWithMessage("targetType should be SELECTION_TYPE_SERVICE") + .that(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertWithMessage( + "The packages shouldn't match for app target and direct target") + .that(call.getDirectTargetAlsoRanked()).isEqualTo(-1); + } + + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(android.R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_eachTabUsesExpectedAdapter() { + int personalProfileTargets = 3; + int otherProfileTargets = 1; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile( + personalProfileTargets + otherProfileTargets, /* userID */ 10); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + onView(withText(R.string.resolver_work_tab)).perform(click()); + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets)); + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Test @Ignore + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(first(allOf( + withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), + isDisplayed()))) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_workProfileDisabled_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_turn_on_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW) + public void testWorkTab_previewIsScrollable() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(300); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createWideBitmap()); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test")); + waitForIdle(); + + onView(withId(com.android.intentresolver.R.id.scrollable_image_preview)) + .check(matches(isDisplayed())); + + onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp()); + waitForIdle(); + + onView(withId(com.android.intentresolver.R.id.chooser_headline_row_container)) + .check(matches(isCompletelyDisplayed())); + onView(withId(com.android.intentresolver.R.id.headline)) + .check(matches(isDisplayed())); + onView(withId(com.android.intentresolver.R.id.scrollable_image_preview)) + .check(matches(not(isDisplayed()))); + } + + @Ignore // b/220067877 + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test @Ignore("b/222124533") + public void testAppTargetLogging() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // TODO(b/222124533): other test cases use a timeout to make sure that the UI is fully + // populated; without one, this test flakes. Ideally we should address the need for a + // timeout everywhere instead of introducing one to fix this particular test. + + assertThat(activity.getAdapter().getCount(), is(2)); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test + public void testDirectTargetLogging() { + 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 + ChooserWrapperActivity activity = + 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)) + .updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + 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(); + + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + activity.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + } + + @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)) + .updateAppTargets(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); + setupResolverControllers(resolvedComponentInfos); + + // Start activity + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + // Thread.sleep shouldn't be a thing in an integration test but it's + // necessary here because of the way the code is structured + Thread.sleep(3000); + + assertThat("Chooser should have 2 app targets", + activity.getAdapter().getCount(), is(2)); + assertThat("Chooser should have no direct targets", + activity.getAdapter().getSelectableServiceTargetCount(), is(0)); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Ignore // b/220067877 + @Test + public void testCopyTextToClipboardLogging() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test @Ignore("b/222124533") + public void testSwitchProfileLogging() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withText(R.string.resolver_personal_tab)).perform(click()); + waitForIdle(); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + + assertNull(chosen[0]); + } + + @Test + public void testOneInitialIntent_noAutolaunch() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(1); + setupResolverControllers(personalResolvedComponentInfos); + Intent chooserIntent = createChooserIntent(createSendTextIntent(), + new Intent[] {new Intent("action.fake")}); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + ResolveInfo ri = createFakeResolveInfo(); + when( + ChooserActivityOverrideData + .getInstance().packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(ri); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + assertNull(chosen[0]); + assertThat(activity + .getPersonalListAdapter().getCallerTargetCount(), is(1)); + } + + @Test + public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 1; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(createSendTextIntent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getCallerTargetCount(), is(2)); + assertThat(activity.getWorkListAdapter().getCallerTargetCount(), is(0)); + } + + @Test + public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testDeduplicateCallerTargetRankedTarget() { + // Create 4 ranked app targets. + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos); + // Create caller target which is duplicate with one of app targets + Intent chooserIntent = createChooserIntent(createSendTextIntent(), + new Intent[] {new Intent("action.fake")}); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(0, + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(ri); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + // Total 4 targets (1 caller target, 3 ranked targets) + assertThat(activity.getAdapter().getCount(), is(4)); + assertThat(activity.getAdapter().getCallerTargetCount(), is(1)); + assertThat(activity.getAdapter().getRankedTargetCount(), is(3)); + } + + @Test + public void test_query_shortcut_loader_for_the_selected_tab() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); + ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); + final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>(); + shortcutLoaders.put(0, personalProfileShortcutLoader); + shortcutLoaders.put(10, workProfileShortcutLoader); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + + verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any()); + + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + verify(workProfileShortcutLoader, times(1)).updateAppTargets(any()); + } + + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + + final IChooserWrapper activity = (IChooserWrapper) mActivityRule + .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest")); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + 4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + } + + private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { + Intent chooserIntent = new Intent(); + chooserIntent.setAction(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + chooserIntent.putExtra(Intent.EXTRA_TITLE, "some title"); + chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); + chooserIntent.setType("text/plain"); + if (initialIntents != null) { + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents); + } + return chooserIntent; + } + + /* This is a "test of a test" to make sure that our inherited test class + * is successfully configured to operate on the unbundled-equivalent + * ChooserWrapperActivity. + * + * TODO: remove after unbundling is complete. + */ + @Test + public void testWrapperActivityHasExpectedConcreteType() { + final ChooserActivity activity = mActivityRule.launchActivity( + Intent.createChooser(new Intent("ACTION_FOO"), "foo")); + waitForIdle(); + assertThat(activity).isInstanceOf(ChooserWrapperActivity.class); + } + + private ResolveInfo createFakeResolveInfo() { + ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = new ActivityInfo(); + ri.activityInfo.name = "FakeActivityName"; + ri.activityInfo.packageName = "fake.package.name"; + ri.activityInfo.applicationInfo = new ApplicationInfo(); + ri.activityInfo.applicationInfo.packageName = "fake.package.name"; + ri.userHandle = UserHandle.CURRENT; + return ri; + } + + private Intent createSendTextIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private Intent createSendImageIntent(Uri imageThumbnail) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail); + sendIntent.setType("image/png"); + if (imageThumbnail != null) { + ClipData.Item clipItem = new ClipData.Item(imageThumbnail); + sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); + } + + return sendIntent; + } + + private Uri createTestContentProviderUri( + @Nullable String mimeType, @Nullable String streamType) { + return createTestContentProviderUri(mimeType, streamType, 0); + } + + private Uri createTestContentProviderUri( + @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) { + String packageName = + InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); + Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png") + .buildUpon(); + if (mimeType != null) { + builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType); + } + if (streamType != null) { + builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType); + } + if (streamTypeTimeout > 0) { + builder.appendQueryParameter( + TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT, + Long.toString(streamTypeTimeout)); + } + return builder.build(); + } + + private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.putExtra(Intent.EXTRA_TITLE, title); + if (imageThumbnail != null) { + ClipData.Item clipItem = new ClipData.Item(imageThumbnail); + sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); + } + + return sendIntent; + } + + private Intent createSendUriIntentWithPreview(ArrayList<Uri> uris) { + Intent sendIntent = new Intent(); + + if (uris.size() > 1) { + sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + sendIntent.putExtra(Intent.EXTRA_STREAM, uris); + } else { + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + + return sendIntent; + } + + private Intent createViewTextIntent() { + Intent viewIntent = new Intent(); + viewIntent.setAction(Intent.ACTION_VIEW); + viewIntent.putExtra(Intent.EXTRA_TEXT, "testing intent viewing"); + return viewIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + PERSONAL_USER_HANDLE)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) { + Icon icon = Icon.createWithBitmap(createBitmap()); + String testTitle = "testTitle"; + List<ChooserTarget> targets = new ArrayList<>(); + for (int i = 0; i < numberOfResults; i++) { + ComponentName componentName; + if (packageName.isEmpty()) { + componentName = ResolverDataProvider.createComponentName(i); + } else { + componentName = new ComponentName(packageName, packageName + ".class"); + } + ChooserTarget tempTarget = new ChooserTarget( + testTitle + i, + icon, + (float) (1 - ((i + 1) / 10.0)), + componentName, + null); + targets.add(tempTarget); + } + return targets; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private Bitmap createBitmap() { + return createBitmap(200, 200); + } + + private Bitmap createWideBitmap() { + return createWideBitmap(Color.RED); + } + + private Bitmap createWideBitmap(int bgColor) { + WindowManager windowManager = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getSystemService(WindowManager.class); + int width = 3000; + if (windowManager != null) { + Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); + width = bounds.width() + 200; + } + return createBitmap(width, 100, bgColor); + } + + private Bitmap createBitmap(int width, int height) { + return createBitmap(width, height, Color.RED); + } + + private Bitmap createBitmap(int width, int height, int bgColor) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint paint = new Paint(); + paint.setColor(bgColor); + paint.setStyle(Paint.Style.FILL); + canvas.drawPaint(paint); + + paint.setColor(Color.WHITE); + paint.setAntiAlias(true); + paint.setTextSize(14.f); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint); + + return bitmap; + } + + private List<ShareShortcutInfo> createShortcuts(Context context) { + Intent testIntent = new Intent("TestIntent"); + + List<ShareShortcutInfo> shortcuts = new ArrayList<>(); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut1") + .setIntent(testIntent).setShortLabel("label1").setRank(3).build(), // 0 2 + new ComponentName("package1", "class1"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut2") + .setIntent(testIntent).setShortLabel("label2").setRank(7).build(), // 1 3 + new ComponentName("package2", "class2"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut3") + .setIntent(testIntent).setShortLabel("label3").setRank(1).build(), // 2 0 + new ComponentName("package3", "class3"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut4") + .setIntent(testIntent).setShortLabel("label4").setRank(3).build(), // 3 2 + new ComponentName("package4", "class4"))); + + return shortcuts; + } + + private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) { + AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder(); + handles + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE); + if (workAvailable) { + handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE); + } + if (cloneAvailable) { + handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE); + } + ChooserWrapperActivity.sOverrides.annotatedUserHandles = handles.build(); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when( + ChooserActivityOverrideData + .getInstance() + .workResolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when( + ChooserActivityOverrideData + .getInstance() + .workResolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.of(10)))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); + } + + private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) { + return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount)); + } + + private static class GridRecyclerSpanCountMatcher extends + BoundedDiagnosingMatcher<View, RecyclerView> { + + private final Matcher<Integer> mIntegerMatcher; + + private GridRecyclerSpanCountMatcher(Matcher<Integer> integerMatcher) { + super(RecyclerView.class); + this.mIntegerMatcher = integerMatcher; + } + + @Override + protected void describeMoreTo(Description description) { + description.appendText("RecyclerView grid layout span count to match: "); + this.mIntegerMatcher.describeTo(description); + } + + @Override + protected boolean matchesSafely(RecyclerView view, Description mismatchDescription) { + int spanCount = ((GridLayoutManager) view.getLayoutManager()).getSpanCount(); + if (this.mIntegerMatcher.matches(spanCount)) { + return true; + } else { + mismatchDescription.appendText("RecyclerView grid layout span count was ") + .appendValue(spanCount); + return false; + } + } + } + + private void givenAppTargets(int appCount) { + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTest(appCount); + setupResolverControllers(resolvedComponentInfos); + } + + private void updateMaxTargetsPerRowResource(int targetsPerRow) { + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(targetsPerRow).when(resources).getInteger( + R.integer.config_chooser_max_targets_per_row); + } + + private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> + createShortcutLoaderFactory() { + 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; + }; + return shortcutLoaders; + } + + private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { + return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); + } +} diff --git a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java new file mode 100644 index 00000000..da879f74 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -0,0 +1,480 @@ +/* + * 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 static android.testing.PollingCheck.waitFor; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isSelected; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.ChooserWrapperActivity.sOverrides; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK; + +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.companion.DeviceFilter; +import android.content.Intent; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; + +import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import dagger.hilt.android.testing.HiltAndroidRule; +import dagger.hilt.android.testing.HiltAndroidTest; + +@DeviceFilter.MediumType +@RunWith(Parameterized.class) +@HiltAndroidTest +public class UnbundledChooserActivityWorkProfileTest { + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + + @Rule(order = 0) + public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this); + + @Rule(order = 1) + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, + false); + private final TestCase mTestCase; + + public UnbundledChooserActivityWorkProfileTest(TestCase testCase) { + mTestCase = testCase; + } + + @Before + public void cleanOverrideData() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void testBlocker() { + setUpPersonalAndWorkComponentInfos(); + sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); + + launchActivity(mTestCase.getIsSendAction()); + switchToTab(mTestCase.getTab()); + + switch (mTestCase.getExpectedBlocker()) { + case NO_BLOCKER: + assertNoBlockerDisplayed(); + break; + case PERSONAL_PROFILE_SHARE_BLOCKER: + assertCantSharePersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_SHARE_BLOCKER: + assertCantShareWorkAppsBlockerDisplayed(); + break; + case PERSONAL_PROFILE_ACCESS_BLOCKER: + assertCantAccessPersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_ACCESS_BLOCKER: + assertCantAccessWorkAppsBlockerDisplayed(); + break; + } + } + + @Parameterized.Parameters(name = "{0}") + public static Collection tests() { + return Arrays.asList( + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ) + ); + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId, UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add( + ResolverDataProvider + .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private void setUpPersonalAndWorkComponentInfos() { + ChooserWrapperActivity.sOverrides.annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(mTestCase.getMyUserHandle()) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE) + .setWorkProfileUserHandle(WORK_USER_HANDLE) + .build(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, + /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .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() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void assertCantAccessWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantAccessPersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantShareWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantSharePersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertNoBlockerDisplayed() { + try { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(not(isDisplayed()))); + } catch (NoMatchingViewException ignored) { + } + } + + private void switchToTab(Tab tab) { + final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab + : R.string.resolver_personal_tab; + + waitFor(() -> { + onView(withText(stringId)).perform(click()); + waitForIdle(); + + try { + onView(withText(stringId)).check(matches(isSelected())); + return true; + } catch (AssertionFailedError e) { + return false; + } + }); + + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + } + + private Intent createTextIntent(boolean isSendAction) { + Intent sendIntent = new Intent(); + if (isSendAction) { + sendIntent.setAction(Intent.ACTION_SEND); + } + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private void launchActivity(boolean isSendAction) { + Intent sendIntent = createTextIntent(isSendAction); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + } + + public static class TestCase { + private final boolean mIsSendAction; + private final boolean mHasCrossProfileIntents; + private final UserHandle mMyUserHandle; + private final Tab mTab; + private final ExpectedBlocker mExpectedBlocker; + + public enum ExpectedBlocker { + NO_BLOCKER, + PERSONAL_PROFILE_SHARE_BLOCKER, + WORK_PROFILE_SHARE_BLOCKER, + PERSONAL_PROFILE_ACCESS_BLOCKER, + WORK_PROFILE_ACCESS_BLOCKER + } + + public enum Tab { + WORK, + PERSONAL + } + + public TestCase(boolean isSendAction, boolean hasCrossProfileIntents, + UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) { + mIsSendAction = isSendAction; + mHasCrossProfileIntents = hasCrossProfileIntents; + mMyUserHandle = myUserHandle; + mTab = tab; + mExpectedBlocker = expectedBlocker; + } + + public boolean getIsSendAction() { + return mIsSendAction; + } + + public boolean hasCrossProfileIntents() { + return mHasCrossProfileIntents; + } + + public UserHandle getMyUserHandle() { + return mMyUserHandle; + } + + public Tab getTab() { + return mTab; + } + + public ExpectedBlocker getExpectedBlocker() { + return mExpectedBlocker; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("test"); + + if (mTab == WORK) { + result.append("WorkTab_"); + } else { + result.append("PersonalTab_"); + } + + if (mIsSendAction) { + result.append("sendAction_"); + } else { + result.append("notSendAction_"); + } + + if (mHasCrossProfileIntents) { + result.append("hasCrossProfileIntents_"); + } else { + result.append("doesNotHaveCrossProfileIntents_"); + } + + if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) { + result.append("myUserIsPersonal_"); + } else { + result.append("myUserIsWork_"); + } + + if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) { + result.append("thenNoBlocker"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnWorkProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnWorkProfile"); + } + + return result.toString(); + } + } +} diff --git a/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt new file mode 100644 index 00000000..cd808af4 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt @@ -0,0 +1,39 @@ +/* + * 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.logging + +import com.android.internal.logging.InstanceId +import com.android.internal.logging.InstanceIdSequence +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.testing.TestInstallIn + +/** Binds a [FakeEventLog] as [EventLog] in tests. */ +@Module +@TestInstallIn(components = [ActivityComponent::class], replaces = [EventLogModule::class]) +interface TestEventLogModule { + + @Binds @ActivityScoped fun fakeEventLog(impl: FakeEventLog): EventLog + + companion object { + @Provides + fun instanceId(sequence: InstanceIdSequence): InstanceId = sequence.newInstanceId() + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java new file mode 100644 index 00000000..32eabbed --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2021 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.v2; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.os.UserHandle; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.WorkProfileAvailabilityManager; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.shortcuts.ShortcutLoader; + +import java.util.function.Consumer; +import java.util.function.Function; + +import kotlin.jvm.functions.Function2; + +/** + * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing. + * We cannot directly mock the activity created since instrumentation creates it, so instead we use + * this singleton to modify behavior. + */ +public class ChooserActivityOverrideData { + private static ChooserActivityOverrideData sInstance = null; + + public static ChooserActivityOverrideData getInstance() { + if (sInstance == null) { + sInstance = new ChooserActivityOverrideData(); + } + return sInstance; + } + + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<TargetInfo, Boolean> onSafelyStartInternalCallback; + public Function<TargetInfo, Boolean> onSafelyStartCallback; + public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> + shortcutLoaderFactory = (userHandle, callback) -> null; + public ChooserActivity.ChooserListController resolverListController; + public ChooserActivity.ChooserListController workResolverListController; + public Boolean isVoiceInteraction; + public Cursor resolverCursor; + public boolean resolverForceException; + public ImageLoader imageLoader; + public int alternateProfileSetting; + public Resources resources; + public AnnotatedUserHandles annotatedUserHandles; + public boolean hasCrossProfileIntents; + public boolean isQuietModeEnabled; + public Integer myUserId; + public WorkProfileAvailabilityManager mWorkProfileAvailability; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; + public PackageManager packageManager; + + public void reset() { + onSafelyStartInternalCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + imageLoader = null; + resolverCursor = null; + resolverForceException = false; + resolverListController = mock(ChooserActivity.ChooserListController.class); + workResolverListController = mock(ChooserActivity.ChooserListController.class); + alternateProfileSetting = 0; + resources = null; + annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM) + .setPersonalProfileUserHandle(UserHandle.SYSTEM) + .build(); + hasCrossProfileIntents = true; + isQuietModeEnabled = false; + myUserId = null; + packageManager = null; + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { + @Override + public boolean isQuietModeEnabled() { + return isQuietModeEnabled; + } + + @Override + public boolean isWorkProfileUserUnlocked() { + return true; + } + + @Override + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } + }; + shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); + } + + private ChooserActivityOverrideData() {} +} + diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java new file mode 100644 index 00000000..5572bb24 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2008 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.v2; + +import android.annotation.Nullable; +import android.app.prediction.AppPredictor; +import android.app.usage.UsageStatsManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; + +import androidx.lifecycle.ViewModelProvider; + +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.ChooserRequestParameters; +import com.android.intentresolver.IChooserWrapper; +import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.TestContentPreviewViewModel; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.icons.TargetDataLoader; +import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Simple wrapper around chooser activity to be able to initiate it under test. For more + * information, see {@code com.android.internal.app.ChooserWrapperActivity}. + */ +public class ChooserWrapperActivity extends ChooserActivity implements IChooserWrapper { + static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance(); + private UsageStatsManager mUsm; + + public ChooserWrapperActivity() { + super(); + mLogic = new TestChooserActivityLogic( + "ChooserWrapper", + () -> this, + this::onWorkProfileStatusUpdated, + () -> mTargetDataLoader, + this::onPreinitialization, + sOverrides + ); + } + + // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at + // onCreate and needs to see some non-negative value in the test. + @Override + public int getLaunchedFromUid() { + return 1234; + } + + @Override + public ChooserListAdapter createChooserListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + UserHandle userHandle, + Intent targetIntent, + ChooserRequestParameters chooserRequest, + int maxTargetsPerRow, + TargetDataLoader targetDataLoader) { + PackageManager packageManager = + sOverrides.packageManager == null ? context.getPackageManager() + : sOverrides.packageManager; + return new ChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + userHandle, + targetIntent, + this, + packageManager, + getEventLog(), + chooserRequest, + maxTargetsPerRow, + userHandle, + targetDataLoader); + } + + @Override + public ChooserListAdapter getAdapter() { + return mChooserMultiProfilePagerAdapter.getActiveListAdapter(); + } + + @Override + public ChooserListAdapter getPersonalListAdapter() { + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)) + .getListAdapter(); + } + + @Override + public ChooserListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)) + .getListAdapter(); + } + + @Override + public boolean getIsSelected() { + return mIsSuccessfullySelected; + } + + @Override + public UsageStatsManager getUsageStatsManager() { + if (mUsm == null) { + mUsm = getSystemService(UsageStatsManager.class); + } + return mUsm; + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(cti)) { + return; + } + super.safelyStartActivityInternal(cti, user, options); + } + + @Override + protected ChooserListController createListController(UserHandle userHandle) { + if (userHandle == UserHandle.SYSTEM) { + return sOverrides.resolverListController; + } + return sOverrides.workResolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + @Override + public Resources getResources() { + if (sOverrides.resources != null) { + return sOverrides.resources; + } + return super.getResources(); + } + + @Override + protected ViewModelProvider.Factory createPreviewViewModelFactory() { + return TestContentPreviewViewModel.Companion.wrap( + super.createPreviewViewModelFactory(), + sOverrides.imageLoader); + } + + @Override + public Cursor queryResolver(ContentResolver resolver, Uri uri) { + if (sOverrides.resolverCursor != null) { + return sOverrides.resolverCursor; + } + + if (sOverrides.resolverForceException) { + throw new SecurityException("Test exception handling"); + } + + return super.queryResolver(resolver, uri); + } + + @Override + protected boolean isWorkProfile() { + if (sOverrides.alternateProfileSetting != 0) { + return sOverrides.alternateProfileSetting == MetricsEvent.MANAGED_PROFILE; + } + return super.isWorkProfile(); + } + + @Override + public DisplayResolveInfo createTestDisplayResolveInfo( + Intent originalIntent, + ResolveInfo pri, + CharSequence pLabel, + CharSequence pInfo, + Intent replacementIntent) { + return DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + pri, + pLabel, + pInfo, + replacementIntent); + } + + @Override + public UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @Override + public Context createContextAsUser(UserHandle user, int flags) { + // return the current context as a work profile doesn't really exist in these tests + return this; + } + + @Override + protected ShortcutLoader createShortcutLoader( + Context context, + AppPredictor appPredictor, + UserHandle userHandle, + IntentFilter targetIntentFilter, + Consumer<ShortcutLoader.Result> callback) { + ShortcutLoader shortcutLoader = + sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); + if (shortcutLoader != null) { + return shortcutLoader; + } + return super.createShortcutLoader( + context, appPredictor, userHandle, targetIntentFilter, callback); + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java b/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java new file mode 100644 index 00000000..f0911833 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java @@ -0,0 +1,1105 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.android.intentresolver.MatcherUtils.first; +import static com.android.intentresolver.v2.ResolverWrapperActivity.sOverrides; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.R; +import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.ResolverDataProvider; +import com.android.intentresolver.widget.ResolverDrawerLayout; +import com.google.android.collect.Lists; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +/** + * Resolver activity instrumentation tests + */ +@RunWith(AndroidJUnit4.class) +public class ResolverActivityTest { + + private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app + .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10); + private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11); + + protected Intent getConcreteIntentForLaunch(Intent clientIntent) { + clientIntent.setClass( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(), + ResolverWrapperActivity.class); + return clientIntent; + } + + @Rule + public ActivityTestRule<ResolverWrapperActivity> mActivityRule = + new ActivityTestRule<>(ResolverWrapperActivity.class, false, false); + + @Before + public void setup() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Ignore // Failing - b/144929805 + @Test + public void setMaxHeight() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final int initialResolverHeight = viewPager.getHeight(); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight - 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should be capped at maxHeight", + viewPager.getHeight() == (initialResolverHeight - 1)); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight + 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should not change height if its height is less than maxHeight", + viewPager.getHeight() == initialResolverHeight); + } + + @Ignore // Failing - b/144929805 + @Test + public void setShowAtTopToTrue() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final View divider = activity.findViewById(com.android.internal.R.id.divider); + final RelativeLayout profileView = + (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button) + .getParent(); + assertThat("Drawer should show at bottom by default", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() > 0); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + com.android.internal.R.id.contentPanel); + layout.setShowAtTop(true); + }); + waitForIdle(); + assertThat("Drawer should show at top with new attribute", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() == 0); + } + + @Test + public void hasLastChosenActivity() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileOneOption() throws Exception { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + + ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); + Intent sendIntent = createSendImageIntent(); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10, + PERSONAL_USER_HANDLE); + // We pick the first one as there is another one in the work profile side + onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + + @Test + public void hasLastChosenActivityAndOtherProfile() throws Exception { + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + Intent sendIntent = createSendImageIntent(); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + Intent sendIntent = createSendImageIntent(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, + new ArrayList<>(workResolvedComponentInfos)); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + // The work list adapter must be populated in advance before tapping the other tab + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_workTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_personalTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(2)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets() + throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_headerIsVisibleInPersonalTab() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); + String initialText = headerText.getText().toString(); + assertFalse("Header text is empty.", initialText.isEmpty()); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + } + + @Test + public void testWorkTab_switchTabs_headerStaysSame() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); + String initialText = headerText.getText().toString(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + String currentText = headerText.getText().toString(); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + assertThat(String.format("Header text is not the same when switching tabs, personal profile" + + " header was %s but work profile header is %s", initialText, currentText), + TextUtils.equals(initialText, currentText)); + } + + @Test + public void testWorkTab_noPersonalApps_canStartWorkApps() + throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf( + withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), + isDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_workProfileDisabled_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.isQuietModeEnabled = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_turn_on_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + sOverrides.hasCrossProfileIntents = false; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE); + // Personal profile only has a browser + personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver_noCurrentProfileTarget() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Need to ensure mini resolver doesn't trigger here. + assertNotMiniResolver(); + } + + private void assertNotMiniResolver() { + try { + onView(withId(com.android.internal.R.id.open_cross_profile)) + .check(matches(isDisplayed())); + } catch (NoMatchingViewException e) { + return; + } + fail("Mini resolver present but shouldn't be"); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertNull(chosen[0]); + } + + @Test + public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 2, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + + onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled())); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + } + + @Test + public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(first(allOf(withText(personalResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers() + throws Exception { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + List<UserHandle> result = activity + .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE); + + assertThat(result.containsAll( + Lists.newArrayList(PERSONAL_USER_HANDLE, CLONE_PROFILE_USER_HANDLE)), is(true)); + } + + private Intent createSendImageIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("image/jpeg"); + return sendIntent; + } + + private Intent createOpenWebsiteIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_VIEW); + sendIntent.setData(Uri.parse("https://google.com")); + return sendIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + resolvedForUser)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId, UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + resolvedForUser)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + } + return infoList; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) { + AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder(); + handles + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE); + if (workAvailable) { + handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE); + } + if (cloneAvailable) { + handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE); + } + sOverrides.annotatedUserHandles = handles.build(); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .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/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java new file mode 100644 index 00000000..92b73d92 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2017 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.v2; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.util.Pair; + +import androidx.annotation.NonNull; +import androidx.test.espresso.idling.CountingIdlingResource; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.WorkProfileAvailabilityManager; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.SelectableTargetInfo; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.icons.LabelInfo; +import com.android.intentresolver.icons.TargetDataLoader; + +import kotlin.Unit; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +/* + * Simple wrapper around chooser activity to be able to initiate it under test + */ +public class ResolverWrapperActivity extends ResolverActivity { + static final OverrideData sOverrides = new OverrideData(); + + private final CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); + + public ResolverWrapperActivity() { + super(/* isIntentPicker= */ true); + mLogic = new TestResolverActivityLogic( + "ResolverWrapper", + () -> this, + () -> { + onWorkProfileStatusUpdated(); + return Unit.INSTANCE; + }, + sOverrides + ); + } + + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + + @Override + public ResolverListAdapter createResolverListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + UserHandle userHandle, + TargetDataLoader targetDataLoader) { + return new ResolverListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + userHandle, + payloadIntents.get(0), // TODO: extract upstream + this, + userHandle, + new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource)); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + ResolverListAdapter getAdapter() { + return mMultiProfilePagerAdapter.getActiveListAdapter(); + } + + ResolverListAdapter getPersonalListAdapter() { + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)); + } + + ResolverListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)); + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) { + return; + } + super.safelyStartActivityInternal(cti, user, options); + } + + @Override + protected ResolverListController createListController(UserHandle userHandle) { + if (userHandle == UserHandle.SYSTEM) { + return sOverrides.resolverListController; + } + return sOverrides.workResolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + protected UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @Override + public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { + super.startActivityAsUser(intent, options, user); + } + + @Override + protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle + userHandle) { + return super.getResolverRankerServiceUserHandleListInternal(userHandle); + } + + /** + * We cannot directly mock the activity created since instrumentation creates it. + * <p> + * Instead, we use static instances of this object to modify behavior. + */ + public static class OverrideData { + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback; + public ResolverListController resolverListController; + public ResolverListController workResolverListController; + public Boolean isVoiceInteraction; + public AnnotatedUserHandles annotatedUserHandles; + public Integer myUserId; + public boolean hasCrossProfileIntents; + public boolean isQuietModeEnabled; + public WorkProfileAvailabilityManager mWorkProfileAvailability; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; + + public void reset() { + onSafelyStartInternalCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + resolverListController = mock(ResolverListController.class); + workResolverListController = mock(ResolverListController.class); + annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM) + .setPersonalProfileUserHandle(UserHandle.SYSTEM) + .build(); + myUserId = null; + hasCrossProfileIntents = true; + isQuietModeEnabled = false; + + mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) { + @Override + public boolean isQuietModeEnabled() { + return isQuietModeEnabled; + } + + @Override + public boolean isWorkProfileUserUnlocked() { + return true; + } + + @Override + public void requestQuietModeEnabled(boolean enabled) { + isQuietModeEnabled = enabled; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() {} + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); + } + } + + private static class TargetDataLoaderWrapper extends TargetDataLoader { + private final TargetDataLoader mTargetDataLoader; + private final CountingIdlingResource mLabelIdlingResource; + + private TargetDataLoaderWrapper( + TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) { + mTargetDataLoader = targetDataLoader; + mLabelIdlingResource = labelIdlingResource; + } + + @Override + public void loadAppTargetIcon( + @NonNull DisplayResolveInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback); + } + + @Override + public void loadDirectShareIcon( + @NonNull SelectableTargetInfo info, + @NonNull UserHandle userHandle, + @NonNull Consumer<Drawable> callback) { + mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback); + } + + @Override + public void loadLabel( + @NonNull DisplayResolveInfo info, + @NonNull Consumer<LabelInfo> callback) { + mLabelIdlingResource.increment(); + mTargetDataLoader.loadLabel( + info, + (result) -> { + mLabelIdlingResource.decrement(); + callback.accept(result); + }); + } + + @Override + public void getOrLoadLabel(@NonNull DisplayResolveInfo info) { + mTargetDataLoader.getOrLoadLabel(info); + } + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt new file mode 100644 index 00000000..198b9236 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt @@ -0,0 +1,32 @@ +package com.android.intentresolver.v2 + +import androidx.activity.ComponentActivity +import com.android.intentresolver.AnnotatedUserHandles +import com.android.intentresolver.WorkProfileAvailabilityManager +import com.android.intentresolver.icons.TargetDataLoader + +/** Activity logic for use when testing [ChooserActivity]. */ +class TestChooserActivityLogic( + tag: String, + activityProvider: () -> ComponentActivity, + onWorkProfileStatusUpdated: () -> Unit, + targetDataLoaderProvider: () -> TargetDataLoader, + onPreinitialization: () -> Unit, + private val overrideData: ChooserActivityOverrideData, +) : + ChooserActivityLogic( + tag, + activityProvider, + onWorkProfileStatusUpdated, + targetDataLoaderProvider, + onPreinitialization, + ) { + + override val annotatedUserHandles: AnnotatedUserHandles? by lazy { + overrideData.annotatedUserHandles + } + + override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy { + overrideData.mWorkProfileAvailability ?: super.workProfileAvailabilityManager + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt new file mode 100644 index 00000000..7581043e --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt @@ -0,0 +1,22 @@ +package com.android.intentresolver.v2 + +import androidx.activity.ComponentActivity +import com.android.intentresolver.AnnotatedUserHandles +import com.android.intentresolver.WorkProfileAvailabilityManager + +/** Activity logic for use when testing [ResolverActivity]. */ +class TestResolverActivityLogic( + tag: String, + activityProvider: () -> ComponentActivity, + onWorkProfileStatusUpdated: () -> Unit, + private val overrideData: ResolverWrapperActivity.OverrideData, +) : ResolverActivityLogic(tag, activityProvider, onWorkProfileStatusUpdated) { + + override val annotatedUserHandles: AnnotatedUserHandles? by lazy { + overrideData.annotatedUserHandles + } + + override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy { + overrideData.mWorkProfileAvailability ?: super.workProfileAvailabilityManager + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java new file mode 100644 index 00000000..5245f655 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java @@ -0,0 +1,3147 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2; + +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; +import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; +import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; +import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; +import static com.android.intentresolver.MatcherUtils.first; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static junit.framework.Assert.assertNull; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +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; +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; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.DeviceConfig; +import android.service.chooser.ChooserAction; +import android.service.chooser.ChooserTarget; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.BackgroundColorSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.text.style.UnderlineSpan; +import android.util.Pair; +import android.util.SparseArray; +import android.view.View; +import android.view.WindowManager; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.test.espresso.contrib.RecyclerViewActions; +import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; +import androidx.test.espresso.matcher.ViewMatchers; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.rule.ActivityTestRule; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.Flags; +import com.android.intentresolver.IChooserWrapper; +import com.android.intentresolver.R; +import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.ResolverDataProvider; +import com.android.intentresolver.TestContentProvider; +import com.android.intentresolver.TestPreviewImageLoader; +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.logging.EventLog; +import com.android.intentresolver.logging.FakeEventLog; +import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.intentresolver.v2.platform.ImageEditor; +import com.android.intentresolver.v2.platform.ImageEditorModule; +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; + +import dagger.hilt.android.testing.BindValue; +import dagger.hilt.android.testing.HiltAndroidRule; +import dagger.hilt.android.testing.HiltAndroidTest; +import dagger.hilt.android.testing.UninstallModules; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Instrumentation tests for ChooserActivity. + * <p> + * Legacy test suite migrated from framework CoreTests. + */ +@RunWith(Parameterized.class) +@HiltAndroidTest +@UninstallModules(ImageEditorModule.class) +public class UnbundledChooserActivityTest { + + private static FakeEventLog getEventLog(ChooserWrapperActivity activity) { + return (FakeEventLog) activity.mEventLog; + } + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10); + private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11); + + private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm; + private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM = + pm -> { + PackageManager mock = Mockito.spy(pm); + when(mock.getAppPredictionServicePackageName()).thenReturn(null); + return mock; + }; + + @Parameterized.Parameters + public static Collection packageManagers() { + return Arrays.asList(new Object[][] { + // Default PackageManager + { DEFAULT_PM }, + // No App Prediction Service + { NO_APP_PREDICTION_SERVICE_PM} + }); + } + + private static final String TEST_MIME_TYPE = "application/TestType"; + + private static final int CONTENT_PREVIEW_IMAGE = 1; + private static final int CONTENT_PREVIEW_FILE = 2; + private static final int CONTENT_PREVIEW_TEXT = 3; + + @Rule(order = 0) + public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule(order = 1) + public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this); + + @Rule(order = 2) + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, false); + + @Before + public void setUp() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + cleanOverrideData(); + mHiltAndroidRule.inject(); + } + + private final Function<PackageManager, PackageManager> mPackageManagerOverride; + + /** An arbitrary pre-installed activity that handles this type of intent. */ + @BindValue + @ImageEditor + final Optional<ComponentName> mImageEditor = Optional.ofNullable( + ComponentName.unflattenFromString("com.google.android.apps.messaging/" + + ".ui.conversationlist.ShareIntentActivity")); + + public UnbundledChooserActivityTest( + Function<PackageManager, PackageManager> packageManagerOverride) { + mPackageManagerOverride = packageManagerOverride; + } + + private void setDeviceConfigProperty( + @NonNull String propertyName, + @NonNull String value) { + // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly + // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently + // configure in {@link #setup()}. + // TODO: is it really appropriate that this is always set with makeDefault=true? + boolean valueWasSet = DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SYSTEMUI, + propertyName, + value, + true /* makeDefault */); + if (!valueWasSet) { + throw new IllegalStateException( + "Could not set " + propertyName + " to " + value); + } + } + + public void cleanOverrideData() { + ChooserActivityOverrideData.getInstance().reset(); + ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride; + + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(true)); + } + + @Test + public void customTitle() throws InterruptedException { + Intent viewIntent = createViewTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity( + Intent.createChooser(viewIntent, "chooser test")); + + waitForIdle(); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getServiceTargetCount(), is(0)); + onView(withId(android.R.id.title)).check(matches(withText("chooser test"))); + } + + @Test + public void customTitleIgnoredForSendIntents() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); + waitForIdle(); + onView(withId(android.R.id.title)) + .check(matches(withText(R.string.whichSendApplication))); + } + + @Test + public void emptyTitle() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(android.R.id.title)) + .check(matches(withText(R.string.whichSendApplication))); + } + + @Test + public void test_shareRichTextWithRichTitle_richTextAndRichTitleDisplayed() { + CharSequence title = new SpannableStringBuilder() + .append("Rich", new UnderlineSpan(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + "Title", + new ForegroundColorSpan(Color.RED), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + CharSequence sharedText = new SpannableStringBuilder() + .append( + "Rich", + new BackgroundColorSpan(Color.YELLOW), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + "Text", + new StyleSpan(Typeface.ITALIC), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + Intent sendIntent = createSendTextIntent(); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + sendIntent.putExtra(Intent.EXTRA_TITLE, title); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.content_preview_title)) + .check((view, e) -> { + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + assertThat(spanned.getSpans(0, spanned.length(), Object.class)) + .hasLength(2); + assertThat(spanned.getSpans(0, 4, UnderlineSpan.class)).hasLength(1); + assertThat(spanned.getSpans(4, spanned.length(), ForegroundColorSpan.class)) + .hasLength(1); + }); + + onView(withId(com.android.internal.R.id.content_preview_text)) + .check((view, e) -> { + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + assertThat(spanned.getSpans(0, spanned.length(), Object.class)) + .hasLength(2); + assertThat(spanned.getSpans(0, 4, BackgroundColorSpan.class)).hasLength(1); + assertThat(spanned.getSpans(4, spanned.length(), StyleSpan.class)).hasLength(1); + }); + } + + @Test + public void emptyPreviewTitleAndThumbnail() throws InterruptedException { + Intent sendIntent = createSendTextIntentWithPreview(null, null); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(not(isDisplayed()))); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleWithoutThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(withText(previewTitle))); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleWithInvalidThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, + Uri.parse("tel:(+49)12345789")); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(not(isDisplayed()))); + } + + @Test + public void visiblePreviewTitleAndThumbnail() throws InterruptedException { + String previewTitle = "My Content Preview Title"; + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + com.android.intentresolver.tests.R.drawable.test320x240); + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) + .check(matches(isDisplayed())); + } + + @Test @Ignore + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void fourOptionsStackedIntoOneTarget() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + + // create just enough targets to ensure the a-z list should be shown + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); + + // next create 4 targets in a single app that should be stacked into a single target + String packageName = "xxx.yyy"; + String appName = "aaa"; + ComponentName cn = new ComponentName(packageName, appName); + Intent intent = new Intent("fakeIntent"); + List<ResolvedComponentInfo> infosToStack = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i, + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); + resolveInfo.activityInfo.applicationInfo.name = appName; + resolveInfo.activityInfo.applicationInfo.packageName = packageName; + resolveInfo.activityInfo.packageName = packageName; + resolveInfo.activityInfo.name = "ccc" + i; + infosToStack.add(new ResolvedComponentInfo(cn, intent, resolveInfo)); + } + resolvedComponentInfos.addAll(infosToStack); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // expect 1 unique targets + 1 group + 4 ranked app targets + assertThat(activity.getAdapter().getCount(), is(6)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + onView(allOf(withText(appName), hasSibling(withText("")))).perform(click()); + waitForIdle(); + + // clicking will launch a dialog to choose the activity within the app + onView(withText(appName)).check(matches(isDisplayed())); + int i = 0; + for (ResolvedComponentInfo rci: infosToStack) { + onView(withText("ccc" + i)).check(matches(isDisplayed())); + ++i; + } + } + + @Test @Ignore + public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + UsageStatsManager usm = activity.getUsageStatsManager(); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .topK(any(List.class), anyInt()); + assertThat(activity.getIsSelected(), is(false)); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + return true; + }; + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo( + sendIntent, toChoose, "testLabel", "testInfo", sendIntent); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .updateChooserCounts(Mockito.anyString(), any(UserHandle.class), + Mockito.anyString()); + verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) + .updateModel(testDri); + assertThat(activity.getIsSelected(), is(true)); + } + + @Ignore // b/148158199 + @Test + public void noResultsFromPackageManager() { + setupResolverControllers(null); + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper wrapper = (IChooserWrapper) activity; + + waitForIdle(); + assertThat(activity.isFinishing(), is(false)); + + onView(withId(android.R.id.empty)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed()))); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> wrapper.getAdapter().handlePackagesChanged() + ); + // backward compatibility. looks like we finish when data is empty after package change + assertThat(activity.isFinishing(), is(true)); + } + + @Test + public void autoLaunchSingleResult() throws InterruptedException { + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1); + setupResolverControllers(resolvedComponentInfos); + + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat(activity.isFinishing(), is(true)); + } + + @Test @Ignore + public void hasOtherProfileOneOption() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); + Intent sendIntent = createSendTextIntent(); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); + waitForIdle(); + + onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test @Ignore + public void hasLastChosenActivityAndOtherProfile() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(3); + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + @Ignore("b/285309527") + public void testFilePlusTextSharing_ExcludeText() { + Uri uri = createTestContentProviderUri(null, "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testFilePlusTextSharing_RemoveAndAddBackText() { + Uri uri = createTestContentProviderUri("application/pdf", "image/png"); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + final String text = "https://google.com/search?q=google"; + sendIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + onView(withId(R.id.content_preview_text)).check(matches(withText("File only"))); + + onView(withId(R.id.include_text_action)) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.content_preview_text)).check(matches(withText(text))); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + Intent alternativeIntent = createSendTextIntent(); + final String text = "alternative intent"; + alternativeIntent.putExtra(Intent.EXTRA_TEXT, text); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + alternativeIntent, PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + AtomicReference<Intent> launchedIntentRef = new AtomicReference<>(); + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = 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 + @Ignore("b/285309527") + public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() { + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.image_view)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + onView(withId(R.id.content_preview_text)) + .check(matches(allOf(isDisplayed(), withText("Image only")))); + } + + @Test + public void copyTextToClipboard() { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); + ClipboardManager clipboard = (ClipboardManager) activity.getSystemService( + Context.CLIPBOARD_SERVICE); + ClipData clipData = clipboard.getPrimaryClip(); + assertThat(clipData).isNotNull(); + assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending"); + + ClipDescription clipDescription = clipData.getDescription(); + assertThat("text/plain", is(clipDescription.getMimeType(0))); + + assertEquals(mActivityRule.getActivityResult().getResultCode(), RESULT_OK); + } + + @Test + public void copyTextToClipboardLogging() { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.copy)).check(matches(isDisplayed())); + onView(withId(R.id.copy)).perform(click()); + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionSelected()) + .isEqualTo(new FakeEventLog.ActionSelected( + /* targetType = */ EventLog.SELECTION_TYPE_COPY)); + } + + @Test + @Ignore + public void testNearbyShareLogging() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_nearby_button)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test @Ignore + public void testEditImageLogs() { + + Uri uri = createTestContentProviderUri("image/png", null); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + + @Test + public void oneVisibleImagePreview() { + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createWideBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(1)); + assertThat(recyclerView.getChildCount(), is(1)); + View imageView = recyclerView.getChildAt(0); + Rect rect = new Rect(); + boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect); + assertThat( + "image preview view is not fully visible", + isPartiallyVisible + && rect.width() == imageView.getWidth() + && rect.height() == imageView.getHeight()); + }); + } + + @Test + public void allThumbnailsFailedToLoad_hidePreview() { + Uri uri = createTestContentProviderUri("image/jpg", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + } + + @Test(timeout = 4_000) + public void testSlowUriMetadata_fallbackToFilePreview() { + Uri uri = createTestContentProviderUri( + "application/pdf", "image/png", /*streamTypeTimeout=*/8_000); + ArrayList<Uri> uris = new ArrayList<>(1); + uris.add(uri); + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + // The preview type resolution is expected to timeout and default to file preview, otherwise + // the test should timeout. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test(timeout = 4_000) + public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi() { + Uri fileUri = createTestContentProviderUri( + "application/pdf", "application/pdf", /*streamTypeTimeout=*/300); + Uri imageUri = createTestContentProviderUri("application/pdf", "image/png"); + ArrayList<Uri> uris = new ArrayList<>(50); + for (int i = 0; i < 49; i++) { + uris.add(fileUri); + } + uris.add(imageUri); + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(imageUri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + // The preview type resolution is expected to timeout and default to file preview, otherwise + // the test should timeout. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + waitForIdle(); + + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void testManyVisibleImagePreview_ScrollableImagePreview() { + Uri uri = createTestContentProviderUri("image/png", null); + + 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().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .perform(RecyclerViewActions.scrollToLastPosition()) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size())); + }); + } + + @Test(timeout = 4_000) + public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart() { + Uri imgOneUri = createTestContentProviderUri("image/png", null); + Uri imgTwoUri = createTestContentProviderUri("image/png", null) + .buildUpon() + .path("image-2.png") + .build(); + Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 8_000); + ArrayList<Uri> uris = new ArrayList<>(2); + // two large previews to fill the screen and be presented right away and one + // document that would be delayed by the URI metadata reading + uris.add(imgOneUri); + uris.add(imgTwoUri); + uris.add(docUri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + Map<Uri, Bitmap> bitmaps = new HashMap<>(); + bitmaps.put(imgOneUri, createWideBitmap(Color.RED)); + bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN)); + bitmaps.put(docUri, createWideBitmap(Color.BLUE)); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(bitmaps); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + // the preview type is expected to be resolved quickly based on the first provided URI + // metadata. If, instead, it is dependent on the third URI metadata, the test should either + // timeout or (more probably due to inner timeout) default to file preview type; anyway the + // test will fail. + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.scrollable_image_preview)) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getChildCount()).isAtLeast(1); + // the first view is a preview + View imageView = recyclerView.getChildAt(0).findViewById(R.id.image); + assertThat(imageView).isNotNull(); + }) + .perform(RecyclerViewActions.scrollToLastPosition()) + .check((view, exception) -> { + if (exception != null) { + throw exception; + } + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getChildCount()).isAtLeast(1); + // check that the last view is a loading indicator + View loadingIndicator = + recyclerView.getChildAt(recyclerView.getChildCount() - 1); + assertThat(loadingIndicator).isNotNull(); + }); + waitForIdle(); + } + + @Test + public void testImageAndTextPreview() { + final Uri uri = createTestContentProviderUri("image/png", null); + 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().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText)) + .check(matches(isDisplayed())); + } + + @Test + public void test_shareImageWithRichText_RichTextIsDisplayed() { + final Uri uri = createTestContentProviderUri("image/png", null); + final CharSequence sharedText = new SpannableStringBuilder() + .append( + "text-", + new StyleSpan(Typeface.BOLD_ITALIC), + Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + .append( + Long.toString(System.currentTimeMillis()), + new ForegroundColorSpan(Color.RED), + Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText.toString())) + .check(matches(isDisplayed())) + .check((view, e) -> { + if (e != null) { + throw e; + } + assertThat(view).isInstanceOf(TextView.class); + CharSequence text = ((TextView) view).getText(); + assertThat(text).isInstanceOf(Spanned.class); + Spanned spanned = (Spanned) text; + Object[] spans = spanned.getSpans(0, text.length(), Object.class); + assertThat(spans).hasLength(2); + assertThat(spanned.getSpans(0, 5, StyleSpan.class)).hasLength(1); + assertThat(spanned.getSpans(5, text.length(), ForegroundColorSpan.class)) + .hasLength(1); + }); + } + + @Test + public void testTextPreviewWhenTextIsSharedWithMultipleImages() { + final Uri uri = createTestContentProviderUri("image/png", null); + final String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + Mockito.any(UserHandle.class))) + .thenReturn(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withText(sharedText)).check(matches(isDisplayed())); + } + + @Test + public void testOnCreateLogging() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isFalse(); + assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE); + } + + @Test + public void testOnCreateLoggingFromWorkProfile() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ChooserActivityOverrideData.getInstance().alternateProfileSetting = + MetricsEvent.MANAGED_PROFILE; + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isTrue(); + assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE); + } + + @Test + public void testEmptyPreviewLogging() { + Intent sendIntent = createSendTextIntentWithPreview(null, null); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, + "empty preview logger test")); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown(); + assertThat(event).isNotNull(); + assertThat(event.isWorkProfile()).isFalse(); + assertThat(event.getTargetMimeType()).isNull(); + } + + @Test + public void testTitlePreviewLogging() { + Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionShareWithPreview()) + .isEqualTo(new FakeEventLog.ActionShareWithPreview( + /* previewType = */ CONTENT_PREVIEW_TEXT)); + } + + @Test + public void testImagePreviewLogging() { + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getActionShareWithPreview()) + .isEqualTo(new FakeEventLog.ActionShareWithPreview( + /* previewType = */ CONTENT_PREVIEW_IMAGE)); + } + + @Test + public void oneVisibleFilePreview() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + + @Test + public void moreThanOneVisibleFilePreview() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 2 more files"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void contentProviderThrowSecurityException() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + ChooserActivityOverrideData.getInstance().resolverForceException = true; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void contentProviderReturnsNoColumns() throws InterruptedException { + Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf"); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos); + + Cursor cursor = mock(Cursor.class); + when(cursor.getCount()).thenReturn(1); + Mockito.doNothing().when(cursor).close(); + when(cursor.moveToFirst()).thenReturn(true); + when(cursor.getColumnIndex(Mockito.anyString())).thenReturn(-1); + + ChooserActivityOverrideData.getInstance().resolverCursor = cursor; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf"))); + onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed())); + onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 1 more file"))); + onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed())); + } + + @Test + public void testGetBaseScore() { + final float testBaseScore = 0.89f; + + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getScore(Mockito.isA(DisplayResolveInfo.class))) + .thenReturn(testBaseScore); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + final DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo( + sendIntent, + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), + "testLabel", + "testInfo", + sendIntent); + final ChooserListAdapter adapter = activity.getAdapter(); + + assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_CHOOSER_TARGET), is(testBaseScore)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE), + is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); + assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER), + is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test + public void testDirectTargetSelectionLogging() { + 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 = + createShortcutLoaderFactory(); + + // Start activity + ChooserWrapperActivity activity = + 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(-1); + var hashResult = call.getDirectTargetHashed(); + var hash = hashResult == null ? "" : hashResult.hashedString; + assertWithMessage("Hash is not predictable but must be obfuscated") + .that(hash).isNotEqualTo(name); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test + public void testDirectTargetLoggingWithRankedAppTarget() { + 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 = + createShortcutLoaderFactory(); + + // Start activity + ChooserWrapperActivity activity = + 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(0); + } + + @Test + public void testShortcutTargetWithApplyAppLimits() { + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + 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 = + createShortcutLoaderFactory(); + + // 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); + } + + @Test + public void testShortcutTargetWithoutApplyAppLimits() { + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(false)); + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + 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 = + createShortcutLoaderFactory(); + + // 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)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + 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(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 4 targets (2 apps, 2 direct)", + activeAdapter.getCount(), + is(4)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(2)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); + assertThat( + "The display label must match", + activeAdapter.getItem(1).getDisplayLabel(), + is("testTitle1")); + } + + @Test + public void testLaunchWithCallerProvidedTarget() { + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(false)); + // Set up resources + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp); + + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + // set caller-provided target + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + String callerTargetLabel = "Caller Target"; + ChooserTarget[] targets = new ChooserTarget[] { + new ChooserTarget( + callerTargetLabel, + Icon.createWithBitmap(createBitmap()), + 0.1f, + resolvedComponentInfos.get(0).name, + new Bundle()) + }; + chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets); + + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); + + // Start activity + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[0], + new HashMap<>(), + new HashMap<>()); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is(callerTargetLabel)); + + // Switch to work profile and ensure that the target *doesn't* show up there. + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + for (int i = 0; i < activity.getWorkListAdapter().getCount(); i++) { + assertThat( + "Chooser target should not show up in opposite profile", + activity.getWorkListAdapter().getItem(i).getDisplayLabel(), + not(callerTargetLabel)); + } + } + + @Test + 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), + Context.RECEIVER_EXPORTED); + + try { + onView(withText(customActionLabel)).perform(click()); + assertTrue("Timeout waiting for broadcast", + broadcastInvoked.await(5000, TimeUnit.MILLISECONDS)); + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test + 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); + String label = "modify share"; + PendingIntent pendingIntent = PendingIntent.getBroadcast( + testContext, + 123, + new Intent(modifyShareAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap( + createBitmap()), label, pendingIntent).build(); + chooserIntent.putExtra( + Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, + action); + // 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), + Context.RECEIVER_EXPORTED); + + try { + onView(withText(label)).perform(click()); + assertTrue("Timeout waiting for broadcast", + broadcastInvoked.await(5000, TimeUnit.MILLISECONDS)); + + } finally { + testContext.unregisterReceiver(testReceiver); + } + } + + @Test + public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException { + updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4); + givenAppTargets(/* appCount= */ 16); + Intent sendIntent = createSendTextIntent(); + final ChooserActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + updateMaxTargetsPerRowResource(/* targetsPerRow= */ 6); + InstrumentationRegistry.getInstrumentation() + .runOnMainSync(() -> activity.onConfigurationChanged( + InstrumentationRegistry.getInstrumentation() + .getContext().getResources().getConfiguration())); + + waitForIdle(); + onView(withId(com.android.internal.R.id.resolver_list)) + .check(matches(withGridColumnCount(6))); + } + + // This test is too long and too slow and should not be taken as an example for future tests. + @Test @Ignore + public void testDirectTargetLoggingWithAppTargetNotRankedPortrait() + throws InterruptedException { + testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_PORTRAIT, 4); + } + + @Test @Ignore + public void testDirectTargetLoggingWithAppTargetNotRankedLandscape() + throws InterruptedException { + testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_LANDSCAPE, 8); + } + + private void testDirectTargetLoggingWithAppTargetNotRanked( + int orientation, int appTargetsExpected) { + Configuration configuration = + new Configuration(InstrumentationRegistry.getInstrumentation().getContext() + .getResources().getConfiguration()); + configuration.orientation = orientation; + + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(configuration).when(resources).getConfiguration(); + + Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15); + setupResolverControllers(resolvedComponentInfos); + + // Create direct share target + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE); + + // Start activity + ChooserWrapperActivity activity = + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + // Insert the direct share target + Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); + directShareToShortcutInfos.put(serviceTargets.get(0), null); + InstrumentationRegistry.getInstrumentation().runOnMainSync( + () -> activity.getAdapter().addServiceResults( + activity.createTestDisplayResolveInfo(sendIntent, + ri, + "testLabel", + "testInfo", + sendIntent), + serviceTargets, + TARGET_TYPE_CHOOSER_TARGET, + directShareToShortcutInfos, + /* directShareToAppTargets */ null) + ); + + assertThat( + String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)", + appTargetsExpected + 16, appTargetsExpected), + activity.getAdapter().getCount(), is(appTargetsExpected + 16)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat("The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + var invocations = eventLog.getShareTargetSelected(); + assertWithMessage("Only one ShareTargetSelected event logged") + .that(invocations).hasSize(1); + FakeEventLog.ShareTargetSelected call = invocations.get(0); + assertWithMessage("targetType should be SELECTION_TYPE_SERVICE") + .that(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + assertWithMessage( + "The packages shouldn't match for app target and direct target") + .that(call.getDirectTargetAlsoRanked()).isEqualTo(-1); + } + + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(android.R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_eachTabUsesExpectedAdapter() { + int personalProfileTargets = 3; + int otherProfileTargets = 1; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile( + personalProfileTargets + otherProfileTargets, /* userID */ 10); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + onView(withText(R.string.resolver_work_tab)).perform(click()); + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets)); + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); + } + + @Test @Ignore + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + int workProfileTargets = 4; + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(first(allOf( + withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), + isDisplayed()))) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_workProfileDisabled_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_turn_on_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW) + public void testWorkTab_previewIsScrollable() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(300); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + + Uri uri = createTestContentProviderUri("image/png", null); + + ArrayList<Uri> uris = new ArrayList<>(); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createWideBitmap()); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test")); + waitForIdle(); + + onView(withId(com.android.intentresolver.R.id.scrollable_image_preview)) + .check(matches(isDisplayed())); + + onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp()); + waitForIdle(); + + onView(withId(com.android.intentresolver.R.id.chooser_headline_row_container)) + .check(matches(isCompletelyDisplayed())); + onView(withId(com.android.intentresolver.R.id.headline)) + .check(matches(isDisplayed())); + onView(withId(com.android.intentresolver.R.id.scrollable_image_preview)) + .check(matches(not(isDisplayed()))); + } + + @Ignore // b/220067877 + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test @Ignore("b/222124533") + public void testAppTargetLogging() throws InterruptedException { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + // TODO(b/222124533): other test cases use a timeout to make sure that the UI is fully + // populated; without one, this test flakes. Ideally we should address the need for a + // timeout everywhere instead of introducing one to fix this particular test. + + assertThat(activity.getAdapter().getCount(), is(2)); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); + + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + waitForIdle(); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test + public void testDirectTargetLogging() { + 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 + ChooserWrapperActivity activity = + 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)) + .updateAppTargets(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + 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(); + + assertThat("Chooser should have 3 targets (2 apps, 1 direct)", + activity.getAdapter().getCount(), is(3)); + assertThat("Chooser should have exactly one selectable direct target", + activity.getAdapter().getSelectableServiceTargetCount(), is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); + + FakeEventLog eventLog = getEventLog(activity); + assertThat(eventLog.getShareTargetSelected()).hasSize(1); + FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0); + assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE); + } + + @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)) + .updateAppTargets(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); + setupResolverControllers(resolvedComponentInfos); + + // Start activity + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + + // Thread.sleep shouldn't be a thing in an integration test but it's + // necessary here because of the way the code is structured + Thread.sleep(3000); + + assertThat("Chooser should have 2 app targets", + activity.getAdapter().getCount(), is(2)); + assertThat("Chooser should have no direct targets", + activity.getAdapter().getSelectableServiceTargetCount(), is(0)); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Ignore // b/220067877 + @Test + public void testCopyTextToClipboardLogging() throws Exception { + Intent sendIntent = createSendTextIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test @Ignore("b/222124533") + public void testSwitchProfileLogging() throws InterruptedException { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withText(R.string.resolver_personal_tab)).perform(click()); + waitForIdle(); + + // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. + } + + @Test + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + + assertNull(chosen[0]); + } + + @Test + public void testOneInitialIntent_noAutolaunch() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(1); + setupResolverControllers(personalResolvedComponentInfos); + Intent chooserIntent = createChooserIntent(createSendTextIntent(), + new Intent[] {new Intent("action.fake")}); + ResolveInfo[] chosen = new ResolveInfo[1]; + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + ResolveInfo ri = createFakeResolveInfo(); + when( + ChooserActivityOverrideData + .getInstance().packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(ri); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + assertNull(chosen[0]); + assertThat(activity + .getPersonalListAdapter().getCallerTargetCount(), is(1)); + } + + @Test + public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 1; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(createSendTextIntent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getCallerTargetCount(), is(2)); + assertThat(activity.getWorkListAdapter().getCallerTargetCount(), is(0)); + } + + @Test + public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent[] initialIntents = { + new Intent("action.fake1"), + new Intent("action.fake2") + }; + Intent chooserIntent = createChooserIntent(new Intent(), initialIntents); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(createFakeResolveInfo()); + + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testDeduplicateCallerTargetRankedTarget() { + // Create 4 ranked app targets. + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos); + // Create caller target which is duplicate with one of app targets + Intent chooserIntent = createChooserIntent(createSendTextIntent(), + new Intent[] {new Intent("action.fake")}); + ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(0, + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); + when( + ChooserActivityOverrideData + .getInstance() + .packageManager + .resolveActivity(any(Intent.class), any())) + .thenReturn(ri); + waitForIdle(); + + IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent); + waitForIdle(); + + // Total 4 targets (1 caller target, 3 ranked targets) + assertThat(activity.getAdapter().getCount(), is(4)); + assertThat(activity.getAdapter().getCallerTargetCount(), is(1)); + assertThat(activity.getAdapter().getRankedTargetCount(), is(3)); + } + + @Test + public void test_query_shortcut_loader_for_the_selected_tab() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(3); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); + ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); + final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>(); + shortcutLoaders.put(0, personalProfileShortcutLoader); + shortcutLoaders.put(10, workProfileShortcutLoader); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + waitForIdle(); + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + + verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any()); + + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + verify(workProfileShortcutLoader, times(1)).updateAppTargets(any()); + } + + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + CLONE_PROFILE_USER_HANDLE); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + + final IChooserWrapper activity = (IChooserWrapper) mActivityRule + .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest")); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest( + 4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + } + + private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { + Intent chooserIntent = new Intent(); + chooserIntent.setAction(Intent.ACTION_CHOOSER); + chooserIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + chooserIntent.putExtra(Intent.EXTRA_TITLE, "some title"); + chooserIntent.putExtra(Intent.EXTRA_INTENT, intent); + chooserIntent.setType("text/plain"); + if (initialIntents != null) { + chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents); + } + return chooserIntent; + } + + /* This is a "test of a test" to make sure that our inherited test class + * is successfully configured to operate on the unbundled-equivalent + * ChooserWrapperActivity. + * + * TODO: remove after unbundling is complete. + */ + @Test + public void testWrapperActivityHasExpectedConcreteType() { + final ChooserActivity activity = mActivityRule.launchActivity( + Intent.createChooser(new Intent("ACTION_FOO"), "foo")); + waitForIdle(); + assertThat(activity).isInstanceOf(ChooserWrapperActivity.class); + } + + private ResolveInfo createFakeResolveInfo() { + ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = new ActivityInfo(); + ri.activityInfo.name = "FakeActivityName"; + ri.activityInfo.packageName = "fake.package.name"; + ri.activityInfo.applicationInfo = new ApplicationInfo(); + ri.activityInfo.applicationInfo.packageName = "fake.package.name"; + ri.userHandle = UserHandle.CURRENT; + return ri; + } + + private Intent createSendTextIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private Intent createSendImageIntent(Uri imageThumbnail) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail); + sendIntent.setType("image/png"); + if (imageThumbnail != null) { + ClipData.Item clipItem = new ClipData.Item(imageThumbnail); + sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); + } + + return sendIntent; + } + + private Uri createTestContentProviderUri( + @Nullable String mimeType, @Nullable String streamType) { + return createTestContentProviderUri(mimeType, streamType, 0); + } + + private Uri createTestContentProviderUri( + @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) { + String packageName = + InstrumentationRegistry.getInstrumentation().getContext().getPackageName(); + Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png") + .buildUpon(); + if (mimeType != null) { + builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType); + } + if (streamType != null) { + builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType); + } + if (streamTypeTimeout > 0) { + builder.appendQueryParameter( + TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT, + Long.toString(streamTypeTimeout)); + } + return builder.build(); + } + + private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.putExtra(Intent.EXTRA_TITLE, title); + if (imageThumbnail != null) { + ClipData.Item clipItem = new ClipData.Item(imageThumbnail); + sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem)); + } + + return sendIntent; + } + + private Intent createSendUriIntentWithPreview(ArrayList<Uri> uris) { + Intent sendIntent = new Intent(); + + if (uris.size() > 1) { + sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + sendIntent.putExtra(Intent.EXTRA_STREAM, uris); + } else { + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0)); + } + + return sendIntent; + } + + private Intent createViewTextIntent() { + Intent viewIntent = new Intent(); + viewIntent.setAction(Intent.ACTION_VIEW); + viewIntent.putExtra(Intent.EXTRA_TEXT, "testing intent viewing"); + return viewIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + PERSONAL_USER_HANDLE)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) { + Icon icon = Icon.createWithBitmap(createBitmap()); + String testTitle = "testTitle"; + List<ChooserTarget> targets = new ArrayList<>(); + for (int i = 0; i < numberOfResults; i++) { + ComponentName componentName; + if (packageName.isEmpty()) { + componentName = ResolverDataProvider.createComponentName(i); + } else { + componentName = new ComponentName(packageName, packageName + ".class"); + } + ChooserTarget tempTarget = new ChooserTarget( + testTitle + i, + icon, + (float) (1 - ((i + 1) / 10.0)), + componentName, + null); + targets.add(tempTarget); + } + return targets; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private Bitmap createBitmap() { + return createBitmap(200, 200); + } + + private Bitmap createWideBitmap() { + return createWideBitmap(Color.RED); + } + + private Bitmap createWideBitmap(int bgColor) { + WindowManager windowManager = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getSystemService(WindowManager.class); + int width = 3000; + if (windowManager != null) { + Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); + width = bounds.width() + 200; + } + return createBitmap(width, 100, bgColor); + } + + private Bitmap createBitmap(int width, int height) { + return createBitmap(width, height, Color.RED); + } + + private Bitmap createBitmap(int width, int height, int bgColor) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint paint = new Paint(); + paint.setColor(bgColor); + paint.setStyle(Paint.Style.FILL); + canvas.drawPaint(paint); + + paint.setColor(Color.WHITE); + paint.setAntiAlias(true); + paint.setTextSize(14.f); + paint.setTextAlign(Paint.Align.CENTER); + canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint); + + return bitmap; + } + + private List<ShareShortcutInfo> createShortcuts(Context context) { + Intent testIntent = new Intent("TestIntent"); + + List<ShareShortcutInfo> shortcuts = new ArrayList<>(); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut1") + .setIntent(testIntent).setShortLabel("label1").setRank(3).build(), // 0 2 + new ComponentName("package1", "class1"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut2") + .setIntent(testIntent).setShortLabel("label2").setRank(7).build(), // 1 3 + new ComponentName("package2", "class2"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut3") + .setIntent(testIntent).setShortLabel("label3").setRank(1).build(), // 2 0 + new ComponentName("package3", "class3"))); + shortcuts.add(new ShareShortcutInfo( + new ShortcutInfo.Builder(context, "shortcut4") + .setIntent(testIntent).setShortLabel("label4").setRank(3).build(), // 3 2 + new ComponentName("package4", "class4"))); + + return shortcuts; + } + + private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) { + AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder(); + handles + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE); + if (workAvailable) { + handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE); + } + if (cloneAvailable) { + handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE); + } + ChooserWrapperActivity.sOverrides.annotatedUserHandles = handles.build(); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos) { + setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when( + ChooserActivityOverrideData + .getInstance() + .workResolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when( + ChooserActivityOverrideData + .getInstance() + .workResolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.of(10)))) + .thenReturn(new ArrayList<>(workResolvedComponentInfos)); + } + + private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) { + return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount)); + } + + private static class GridRecyclerSpanCountMatcher extends + BoundedDiagnosingMatcher<View, RecyclerView> { + + private final Matcher<Integer> mIntegerMatcher; + + private GridRecyclerSpanCountMatcher(Matcher<Integer> integerMatcher) { + super(RecyclerView.class); + this.mIntegerMatcher = integerMatcher; + } + + @Override + protected void describeMoreTo(Description description) { + description.appendText("RecyclerView grid layout span count to match: "); + this.mIntegerMatcher.describeTo(description); + } + + @Override + protected boolean matchesSafely(RecyclerView view, Description mismatchDescription) { + int spanCount = ((GridLayoutManager) view.getLayoutManager()).getSpanCount(); + if (this.mIntegerMatcher.matches(spanCount)) { + return true; + } else { + mismatchDescription.appendText("RecyclerView grid layout span count was ") + .appendValue(spanCount); + return false; + } + } + } + + private void givenAppTargets(int appCount) { + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTest(appCount); + setupResolverControllers(resolvedComponentInfos); + } + + private void updateMaxTargetsPerRowResource(int targetsPerRow) { + Resources resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + ChooserActivityOverrideData.getInstance().resources = resources; + doReturn(targetsPerRow).when(resources).getInteger( + R.integer.config_chooser_max_targets_per_row); + } + + private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> + createShortcutLoaderFactory() { + 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; + }; + return shortcutLoaders; + } + + private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { + return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); + } +} diff --git a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java new file mode 100644 index 00000000..e4ec1776 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java @@ -0,0 +1,481 @@ +/* + * 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.v2; + +import static android.testing.PollingCheck.waitFor; +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isSelected; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.android.intentresolver.v2.ChooserWrapperActivity.sOverrides; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL; +import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK; +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.companion.DeviceFilter; +import android.content.Intent; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; + +import com.android.intentresolver.AnnotatedUserHandles; +import com.android.intentresolver.R; +import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.ResolverDataProvider; +import com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import dagger.hilt.android.testing.HiltAndroidRule; +import dagger.hilt.android.testing.HiltAndroidTest; + +@DeviceFilter.MediumType +@RunWith(Parameterized.class) +@HiltAndroidTest +public class UnbundledChooserActivityWorkProfileTest { + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + + @Rule(order = 0) + public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this); + + @Rule(order = 1) + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, + false); + private final TestCase mTestCase; + + public UnbundledChooserActivityWorkProfileTest(TestCase testCase) { + mTestCase = testCase; + } + + @Before + public void cleanOverrideData() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void testBlocker() { + setUpPersonalAndWorkComponentInfos(); + sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); + + launchActivity(mTestCase.getIsSendAction()); + switchToTab(mTestCase.getTab()); + + switch (mTestCase.getExpectedBlocker()) { + case NO_BLOCKER: + assertNoBlockerDisplayed(); + break; + case PERSONAL_PROFILE_SHARE_BLOCKER: + assertCantSharePersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_SHARE_BLOCKER: + assertCantShareWorkAppsBlockerDisplayed(); + break; + case PERSONAL_PROFILE_ACCESS_BLOCKER: + assertCantAccessPersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_ACCESS_BLOCKER: + assertCantAccessWorkAppsBlockerDisplayed(); + break; + } + } + + @Parameterized.Parameters(name = "{0}") + public static Collection tests() { + return Arrays.asList( + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ) + ); + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId, UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add( + ResolverDataProvider + .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private void setUpPersonalAndWorkComponentInfos() { + ChooserWrapperActivity.sOverrides.annotatedUserHandles = AnnotatedUserHandles.newBuilder() + .setUserIdOfCallingApp(1234) // Must be non-negative. + .setUserHandleSharesheetLaunchedAs(mTestCase.getMyUserHandle()) + .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE) + .setWorkProfileUserHandle(WORK_USER_HANDLE) + .build(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, + /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .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() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void assertCantAccessWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantAccessPersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantShareWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantSharePersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertNoBlockerDisplayed() { + try { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(not(isDisplayed()))); + } catch (NoMatchingViewException ignored) { + } + } + + private void switchToTab(Tab tab) { + final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab + : R.string.resolver_personal_tab; + + waitFor(() -> { + onView(withText(stringId)).perform(click()); + waitForIdle(); + + try { + onView(withText(stringId)).check(matches(isSelected())); + return true; + } catch (AssertionFailedError e) { + return false; + } + }); + + onView(withId(com.android.internal.R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + } + + private Intent createTextIntent(boolean isSendAction) { + Intent sendIntent = new Intent(); + if (isSendAction) { + sendIntent.setAction(Intent.ACTION_SEND); + } + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private void launchActivity(boolean isSendAction) { + Intent sendIntent = createTextIntent(isSendAction); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + } + + public static class TestCase { + private final boolean mIsSendAction; + private final boolean mHasCrossProfileIntents; + private final UserHandle mMyUserHandle; + private final Tab mTab; + private final ExpectedBlocker mExpectedBlocker; + + public enum ExpectedBlocker { + NO_BLOCKER, + PERSONAL_PROFILE_SHARE_BLOCKER, + WORK_PROFILE_SHARE_BLOCKER, + PERSONAL_PROFILE_ACCESS_BLOCKER, + WORK_PROFILE_ACCESS_BLOCKER + } + + public enum Tab { + WORK, + PERSONAL + } + + public TestCase(boolean isSendAction, boolean hasCrossProfileIntents, + UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) { + mIsSendAction = isSendAction; + mHasCrossProfileIntents = hasCrossProfileIntents; + mMyUserHandle = myUserHandle; + mTab = tab; + mExpectedBlocker = expectedBlocker; + } + + public boolean getIsSendAction() { + return mIsSendAction; + } + + public boolean hasCrossProfileIntents() { + return mHasCrossProfileIntents; + } + + public UserHandle getMyUserHandle() { + return mMyUserHandle; + } + + public Tab getTab() { + return mTab; + } + + public ExpectedBlocker getExpectedBlocker() { + return mExpectedBlocker; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("test"); + + if (mTab == WORK) { + result.append("WorkTab_"); + } else { + result.append("PersonalTab_"); + } + + if (mIsSendAction) { + result.append("sendAction_"); + } else { + result.append("notSendAction_"); + } + + if (mHasCrossProfileIntents) { + result.append("hasCrossProfileIntents_"); + } else { + result.append("doesNotHaveCrossProfileIntents_"); + } + + if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) { + result.append("myUserIsPersonal_"); + } else { + result.append("myUserIsWork_"); + } + + if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) { + result.append("thenNoBlocker"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnWorkProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnWorkProfile"); + } + + return result.toString(); + } + } +} |