summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Govinda Wasserman <gwasserman@google.com> 2023-09-29 14:54:05 -0400
committer Govinda Wasserman <gwasserman@google.com> 2023-10-03 10:25:19 -0400
commit2bcce186d5b4879e33aedd6bbac7f5b0b4ec9cd8 (patch)
tree6672b5d09cbf3dd72ff89e2692237418aa7e5db5
parent702944b16982cf89d20e421a4278170c6af9a729 (diff)
Hard fork of the ChoserActivity and ResolverActivity
The forked versions are flag guarded by the ChooserSelector. Test: atest com.android.intentresolver Test: adb shell pm resolve-activity -a android.intent.action.CHOOSER Test: Observe that the action resolves to .ChooserActivity Test: adb shell device_config put intentresolver \ com.android.intentresolver.flags.modular_framework true Test: Reboot device Test: adb shell pm resolve-activity -a android.intent.action.CHOOSER Test: Observe that the action resolves to .v2.ChooserActivity BUG: 302113519 Change-Id: I59584ed4649fca754826b17055a41be45a32f326
-rw-r--r--AndroidManifest-app.xml30
-rw-r--r--aconfig/FeatureFlags.aconfig7
-rw-r--r--java/src/com/android/intentresolver/AnnotatedUserHandles.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java4
-rw-r--r--java/src/com/android/intentresolver/MultiProfilePagerAdapter.java26
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java10
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java2
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java18
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java2
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java1851
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserSelector.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivity.java2426
-rw-r--r--java/tests/AndroidManifest.xml2
-rw-r--r--java/tests/src/com/android/intentresolver/MatcherUtils.java2
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java10
-rw-r--r--java/tests/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java131
-rw-r--r--java/tests/src/com/android/intentresolver/v2/ChooserWrapperActivity.java280
-rw-r--r--java/tests/src/com/android/intentresolver/v2/ResolverActivityTest.java1105
-rw-r--r--java/tests/src/com/android/intentresolver/v2/ResolverWrapperActivity.java285
-rw-r--r--java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java3160
-rw-r--r--java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java481
25 files changed, 9837 insertions, 43 deletions
diff --git a/AndroidManifest-app.xml b/AndroidManifest-app.xml
index 9efc7ab1..ec4fec85 100644
--- a/AndroidManifest-app.xml
+++ b/AndroidManifest-app.xml
@@ -60,6 +60,36 @@
android:visibleToInstantApps="true"
android:exported="false"/>
+ <receiver android:name="com.android.intentresolver.v2.ChooserSelector"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name="com.android.intentresolver.v2.ChooserActivity"
+ android:enabled="false"
+ android:theme="@style/Theme.DeviceDefault.Chooser"
+ android:finishOnCloseSystemDialogs="true"
+ android:excludeFromRecents="true"
+ android:documentLaunchMode="never"
+ android:relinquishTaskIdentity="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
+ android:visibleToInstantApps="true"
+ android:exported="true">
+
+ <!-- This intent filter is assigned a priority greater than 500 so
+ that it will take precedence over the ChooserActivity
+ in the process of resolving implicit action.CHOOSER intents
+ whenever this activity is enabled by the experiment flag. -->
+ <intent-filter android:priority="501">
+ <action android:name="android.intent.action.CHOOSER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.VOICE" />
+ </intent-filter>
+
+ </activity>
+
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:replace="android:authorities"
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig
index 7b0ab052..ae83ca79 100644
--- a/aconfig/FeatureFlags.aconfig
+++ b/aconfig/FeatureFlags.aconfig
@@ -24,3 +24,10 @@ flag {
description: "Enables caching target icons and labels in a local DB"
bug: "285314844"
}
+
+flag {
+ name: "modular_framework"
+ namespace: "intentresolver"
+ description: "Enables the new modular framework"
+ bug: "302113519"
+}
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
index 168f36d6..5d559f5b 100644
--- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -105,7 +105,7 @@ public final class AnnotatedUserHandles {
.build();
}
- @VisibleForTesting static Builder newBuilder() {
+ @VisibleForTesting public static Builder newBuilder() {
return new Builder();
}
@@ -173,7 +173,7 @@ public final class AnnotatedUserHandles {
}
@VisibleForTesting
- static class Builder {
+ public static class Builder {
private int mUserIdOfCallingApp;
private UserHandle mUserHandleSharesheetLaunchedAs;
private UserHandle mPersonalProfileUserHandle;
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 3f9e2154..3a11bee2 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1143,7 +1143,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
@Override
- boolean isComponentFiltered(ComponentName name) {
+ public boolean isComponentFiltered(ComponentName name) {
return mChooserRequest.getFilteredComponentNames().contains(name);
}
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 5f373525..aaa7554c 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -70,7 +70,7 @@ public class ChooserGridLayoutManager extends GridLayoutManager {
return super.getRowCountForAccessibility(recycler, state) - 1;
}
- void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
+ public void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
mVerticalScrollEnabled = verticalScrollEnabled;
}
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
index 5fbf03a0..df5a8dc8 100644
--- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -49,7 +49,7 @@ public class ChooserIntegratedDeviceComponents {
}
@VisibleForTesting
- ChooserIntegratedDeviceComponents(
+ public ChooserIntegratedDeviceComponents(
ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
mEditSharingComponent = editSharingComponent;
mNearbySharingComponent = nearbySharingComponent;
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 230c18b2..ec8800b8 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -413,7 +413,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
- void updateAlphabeticalList() {
+ public void updateAlphabeticalList() {
final ChooserActivity.AzInfoComparator comparator =
new ChooserActivity.AzInfoComparator(mContext);
final List<DisplayResolveInfo> allTargets = new ArrayList<>();
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index 23a081d2..080f9d24 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -46,7 +46,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
private final ChooserProfileAdapterBinder mAdapterBinder;
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter adapter,
EmptyStateProvider emptyStateProvider,
@@ -68,7 +68,7 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
featureFlags);
}
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter personalAdapter,
ChooserGridAdapter workAdapter,
diff --git a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
index 8c640dd3..8ce42b28 100644
--- a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
@@ -79,11 +79,11 @@ public class MultiProfilePagerAdapter<
void bind(PageViewT view, SinglePageAdapterT adapter);
}
- static final int PROFILE_PERSONAL = 0;
- static final int PROFILE_WORK = 1;
+ public static final int PROFILE_PERSONAL = 0;
+ public static final int PROFILE_WORK = 1;
@IntDef({PROFILE_PERSONAL, PROFILE_WORK})
- @interface Profile {}
+ public @interface Profile {}
private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
@@ -197,7 +197,7 @@ public class MultiProfilePagerAdapter<
return getItemCount();
}
- protected int getCurrentPage() {
+ public int getCurrentPage() {
return mCurrentPage;
}
@@ -234,7 +234,7 @@ public class MultiProfilePagerAdapter<
return mItems.get(pageIndex);
}
- protected ViewGroup getEmptyStateView(int pageIndex) {
+ public ViewGroup getEmptyStateView(int pageIndex) {
return getItem(pageIndex).getEmptyStateView();
}
@@ -266,7 +266,7 @@ public class MultiProfilePagerAdapter<
* Performs view-related initialization procedures for the adapter specified
* by <code>pageIndex</code>.
*/
- protected final void setupListAdapter(int pageIndex) {
+ public final void setupListAdapter(int pageIndex) {
mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
}
@@ -278,7 +278,7 @@ public class MultiProfilePagerAdapter<
* with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
*/
@Nullable
- protected final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
if (getPersonalListAdapter().getUserHandle().equals(userHandle)
|| userHandle.equals(getCloneUserHandle())) {
return getPersonalListAdapter();
@@ -297,7 +297,7 @@ public class MultiProfilePagerAdapter<
* @see #getInactiveListAdapter()
*/
@VisibleForTesting
- protected final ListAdapterT getActiveListAdapter() {
+ public final ListAdapterT getActiveListAdapter() {
return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
}
@@ -311,7 +311,7 @@ public class MultiProfilePagerAdapter<
*/
@VisibleForTesting
@Nullable
- protected final ListAdapterT getInactiveListAdapter() {
+ public final ListAdapterT getInactiveListAdapter() {
if (getCount() < 2) {
return null;
}
@@ -330,16 +330,16 @@ public class MultiProfilePagerAdapter<
return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
}
- protected final SinglePageAdapterT getCurrentRootAdapter() {
+ public final SinglePageAdapterT getCurrentRootAdapter() {
return getAdapterForIndex(getCurrentPage());
}
- protected final PageViewT getActiveAdapterView() {
+ public final PageViewT getActiveAdapterView() {
return getListViewForIndex(getCurrentPage());
}
@Nullable
- protected final PageViewT getInactiveAdapterView() {
+ public final PageViewT getInactiveAdapterView() {
if (getCount() < 2) {
return null;
}
@@ -505,7 +505,7 @@ public class MultiProfilePagerAdapter<
paddingBottom));
}
- protected void showListView(ListAdapterT activeListAdapter) {
+ public void showListView(ListAdapterT activeListAdapter) {
ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
descriptor.mRootView.findViewById(
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 95ed0d5c..8c0d414c 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -68,7 +68,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected final Context mContext;
protected final LayoutInflater mInflater;
protected final ResolverListCommunicator mResolverListCommunicator;
- protected final ResolverListController mResolverListController;
+ public final ResolverListController mResolverListController;
private final List<Intent> mIntents;
private final Intent[] mInitialIntents;
@@ -229,7 +229,7 @@ public class ResolverListAdapter extends BaseAdapter {
packageName, userHandle, action);
}
- List<ResolvedComponentInfo> getUnfilteredResolveList() {
+ public List<ResolvedComponentInfo> getUnfilteredResolveList() {
return mUnfilteredResolveList;
}
@@ -808,7 +808,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
}
- void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
+ public void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
final DisplayResolveInfo iconInfo = getFilteredItem();
if (iconInfo != null) {
mTargetDataLoader.loadAppTargetIcon(
@@ -834,7 +834,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mIntents;
}
- protected boolean isTabLoaded() {
+ public boolean isTabLoaded() {
return mIsTabLoaded;
}
@@ -893,7 +893,7 @@ public class ResolverListAdapter extends BaseAdapter {
* Necessary methods to communicate between {@link ResolverListAdapter}
* and {@link ResolverActivity}.
*/
- interface ResolverListCommunicator {
+ public interface ResolverListCommunicator {
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index cb56ab30..05121576 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -333,7 +333,7 @@ public class ResolverListController {
&& ai.name.equals(b.name.getClassName());
}
- boolean isComponentFiltered(ComponentName componentName) {
+ public boolean isComponentFiltered(ComponentName componentName) {
return false;
}
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index e0c5380f..591c23b7 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -40,7 +40,7 @@ public class ResolverMultiProfilePagerAdapter extends
MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ResolverMultiProfilePagerAdapter(
+ public ResolverMultiProfilePagerAdapter(
Context context,
ResolverListAdapter adapter,
EmptyStateProvider emptyStateProvider,
@@ -58,14 +58,14 @@ public class ResolverMultiProfilePagerAdapter extends
new BottomPaddingOverrideSupplier());
}
- ResolverMultiProfilePagerAdapter(Context context,
- ResolverListAdapter personalAdapter,
- ResolverListAdapter workAdapter,
- EmptyStateProvider emptyStateProvider,
- Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
- UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle) {
+ public ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
this(
context,
ImmutableList.of(personalAdapter, workAdapter),
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 0804a2b8..0496579d 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -69,7 +69,7 @@ public class ResolverViewPager extends ViewPager {
* Sets whether swiping sideways should happen.
* <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).
*/
- void setSwipingEnabled(boolean swipingEnabled) {
+ public void setSwipingEnabled(boolean swipingEnabled) {
mSwipingEnabled = swipingEnabled;
}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
new file mode 100644
index 00000000..9e437010
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -0,0 +1,1851 @@
+/*
+ * 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 static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+import static com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL;
+import static com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK;
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.widget.TextView;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.ChooserActionFactory;
+import com.android.intentresolver.ChooserGridLayoutManager;
+import com.android.intentresolver.ChooserIntegratedDeviceComponents;
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserMultiProfilePagerAdapter;
+import com.android.intentresolver.ChooserRefinementManager;
+import com.android.intentresolver.ChooserRequestParameters;
+import com.android.intentresolver.ChooserStackedAppDialogFragment;
+import com.android.intentresolver.ChooserTargetActionsDialogFragment;
+import com.android.intentresolver.EnterTransitionAnimationDelegate;
+import com.android.intentresolver.FeatureFlags;
+import com.android.intentresolver.IntentForwarderActivity;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.ResolverViewPager;
+import com.android.intentresolver.SecureSettings;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.BasePreviewViewModel;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
+import com.android.intentresolver.contentpreview.PreviewViewModel;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.icons.DefaultTargetDataLoader;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.measurements.Tracer;
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.shortcuts.AppPredictorFactory;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.v2.Hilt_ChooserActivity;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+/**
+ * The Chooser Activity handles intent resolution specifically for sharing intents -
+ * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
+ *
+ */
+@AndroidEntryPoint(ResolverActivity.class)
+public class ChooserActivity extends Hilt_ChooserActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+ private static final String TAG = "ChooserActivity";
+
+ /**
+ * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
+ * in onStop when launched in a new task. If this extra is set to true, we do not finish
+ * ourselves when onStop gets called.
+ */
+ public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
+ = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
+
+ /**
+ * Transition name for the first image preview.
+ * To be used for shared element transition into this activity.
+ * @hide
+ */
+ public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
+
+ private static final boolean DEBUG = true;
+
+ public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
+ private static final String SHORTCUT_TARGET = "shortcut_target";
+
+ // TODO: these data structures are for one-time use in shuttling data from where they're
+ // populated in `ShortcutToChooserTargetConverter` to where they're consumed in
+ // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
+ // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
+ // intermediate data, and then these members can be removed.
+ private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
+ private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
+
+ public static final int TARGET_TYPE_DEFAULT = 0;
+ public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
+ public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
+ public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+
+ private static final int SCROLL_STATUS_IDLE = 0;
+ private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
+ private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+
+ @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
+ TARGET_TYPE_DEFAULT,
+ TARGET_TYPE_CHOOSER_TARGET,
+ TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShareTargetType {}
+
+ @Inject public FeatureFlags mFeatureFlags;
+ @Inject public EventLog mEventLog;
+
+ private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+
+ /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
+ * only assignment there, and expect it to be ready by the time we ever use it --
+ * someday if we move all the usage to a component with a narrower lifecycle (something that
+ * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we
+ * should be able to make this assignment as "final."
+ */
+ @Nullable
+ private ChooserRequestParameters mChooserRequest;
+
+ private ChooserRefinementManager mRefinementManager;
+
+ private ChooserContentPreviewUi mChooserContentPreviewUi;
+
+ private boolean mShouldDisplayLandscape;
+ private long mChooserShownTime;
+ protected boolean mIsSuccessfullySelected;
+
+ private int mCurrAvailableWidth = 0;
+ private Insets mLastAppliedInsets = null;
+ private int mLastNumberOfChildren = -1;
+ private int mMaxTargetsPerRow = 1;
+
+ private static final int MAX_LOG_RANK_POSITION = 12;
+
+ // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters.
+ private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
+ private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
+
+ private SharedPreferences mPinnedSharedPrefs;
+ private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
+
+ private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
+
+ private int mScrollStatus = SCROLL_STATUS_IDLE;
+
+ @VisibleForTesting
+ protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
+ private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
+ new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
+
+ private View mContentView = null;
+
+ private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
+
+ private boolean mExcludeSharedText = false;
+ /**
+ * When we intend to finish the activity with a shared element transition, we can't immediately
+ * finish() when the transition is invoked, as the receiving end may not be able to start the
+ * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop
+ * in order to wait for the transition to begin.
+ */
+ private boolean mFinishWhenStopped = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Tracer.INSTANCE.markLaunched();
+ final long intentReceivedTime = System.currentTimeMillis();
+ mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+
+ try {
+ mChooserRequest = new ChooserRequestParameters(
+ getIntent(),
+ getReferrerPackageName(),
+ getReferrer());
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
+ finish();
+ super_onCreate(null);
+ return;
+ }
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+ setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ mChooserRequest.getSharedText(),
+ mChooserRequest.getTargetIntentFilter()),
+ mChooserRequest.getTargetIntentFilter());
+
+
+ super.onCreate(
+ savedInstanceState,
+ mChooserRequest.getTargetIntent(),
+ mChooserRequest.getAdditionalTargets(),
+ mChooserRequest.getTitle(),
+ mChooserRequest.getDefaultTitleResource(),
+ mChooserRequest.getInitialIntents(),
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ false,
+ new DefaultTargetDataLoader(this, getLifecycle(), false),
+ /* safeForwardingMode= */ true);
+
+ getEventLog().logSharesheetTriggered();
+
+ mIntegratedDeviceComponents = getIntegratedDeviceComponents();
+
+ mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+
+ mRefinementManager.getRefinementCompletion().observe(this, completion -> {
+ if (completion.consume()) {
+ TargetInfo targetInfo = completion.getTargetInfo();
+ // targetInfo is non-null if the refinement process was successful.
+ if (targetInfo != null) {
+ maybeRemoveSharedText(targetInfo);
+
+ // We already block suspended targets from going to refinement, and we probably
+ // can't recover a Chooser session if that's the reason the refined target fails
+ // to launch now. Fire-and-forget the refined launch; ignore the return value
+ // and just make sure the Sharesheet session gets cleaned up regardless.
+ ChooserActivity.super.onTargetSelected(targetInfo, false);
+ }
+
+ finish();
+ }
+ });
+
+ BasePreviewViewModel previewViewModel =
+ new ViewModelProvider(this, createPreviewViewModelFactory())
+ .get(BasePreviewViewModel.class);
+ mChooserContentPreviewUi = new ChooserContentPreviewUi(
+ getLifecycle(),
+ previewViewModel.createOrReuseProvider(mChooserRequest),
+ mChooserRequest.getTargetIntent(),
+ previewViewModel.createOrReuseImageLoader(),
+ createChooserActionFactory(),
+ mEnterTransitionAnimationDelegate,
+ new HeadlineGeneratorImpl(this));
+
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ getEventLog().logActionShareWithPreview(
+ mChooserContentPreviewUi.getPreferredContentPreview());
+ }
+
+ mChooserShownTime = System.currentTimeMillis();
+ final long systemCost = mChooserShownTime - intentReceivedTime;
+ getEventLog().logChooserActivityShown(
+ isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
+
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
+
+ mResolverDrawerLayout.setOnCollapsedChangedListener(
+ isCollapsed -> {
+ mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
+ });
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "System Time Cost is " + systemCost);
+ }
+
+ getEventLog().logShareStarted(
+ getReferrerPackageName(),
+ mChooserRequest.getTargetType(),
+ mChooserRequest.getCallerChooserTargets().size(),
+ (mChooserRequest.getInitialIntents() == null)
+ ? 0 : mChooserRequest.getInitialIntents().length,
+ isWorkProfile(),
+ mChooserContentPreviewUi.getPreferredContentPreview(),
+ mChooserRequest.getTargetAction(),
+ mChooserRequest.getChooserActions().size(),
+ mChooserRequest.getModifyShareAction() != null
+ );
+
+ mEnterTransitionAnimationDelegate.postponeTransition();
+ }
+
+ @VisibleForTesting
+ protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return ChooserIntegratedDeviceComponents.get(this, new SecureSettings());
+ }
+
+ @Override
+ protected int appliedThemeResId() {
+ return R.style.Theme_DeviceDefault_Chooser;
+ }
+
+ private void createProfileRecords(
+ AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+ UserHandle mainUserHandle = getAnnotatedUserHandles().personalProfileUserHandle;
+ ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+ if (record.shortcutLoader == null) {
+ Tracer.INSTANCE.endLaunchToShortcutTrace();
+ }
+
+ UserHandle workUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
+ if (workUserHandle != null) {
+ createProfileRecord(workUserHandle, targetIntentFilter, factory);
+ }
+ }
+
+ private ProfileRecord createProfileRecord(
+ UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+ AppPredictor appPredictor = factory.create(userHandle);
+ ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
+ ? null
+ : createShortcutLoader(
+ this,
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
+ ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader);
+ mProfileRecords.put(userHandle.getIdentifier(), record);
+ return record;
+ }
+
+ @Nullable
+ private ProfileRecord getProfileRecord(UserHandle userHandle) {
+ return mProfileRecords.get(userHandle.getIdentifier(), null);
+ }
+
+ @VisibleForTesting
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ return new ShortcutLoader(
+ context,
+ getCoroutineScope(getLifecycle()),
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ callback);
+ }
+
+ static SharedPreferences getPinnedSharedPrefs(Context context) {
+ return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);
+ }
+
+ @Override
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ } else {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ }
+ return mChooserMultiProfilePagerAdapter;
+ }
+
+ @Override
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean isSendAction = mChooserRequest.isSendActionTarget();
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation
+ : R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation
+ : R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ChooserGridAdapter adapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ int selectedProfile = findSelectedProfile();
+ ChooserGridAdapter personalAdapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ ChooserGridAdapter workAdapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ getAnnotatedUserHandles().workProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(getAnnotatedUserHandles().workProfileUserHandle),
+ () -> mWorkProfileAvailability.isQuietModeEnabled(),
+ selectedProfile,
+ getAnnotatedUserHandles().workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private int findSelectedProfile() {
+ int selectedProfile = getSelectedProfileExtra();
+ if (selectedProfile == -1) {
+ selectedProfile = getProfileForUser(
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+ return selectedProfile;
+ }
+
+ /**
+ * Check if the profile currently used is a work profile.
+ * @return true if it is work profile, false if it is parent profile (or no work profile is
+ * set up)
+ */
+ protected boolean isWorkProfile() {
+ return getSystemService(UserManager.class)
+ .getUserInfo(UserHandle.myUserId()).isManagedProfile();
+ }
+
+ @Override
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ handlePackagesChanged(listAdapter);
+ }
+ };
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ */
+ public void handlePackagesChanged() {
+ handlePackagesChanged(/* listAdapter */ null);
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if
+ * available.
+ */
+ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) {
+ // Refresh pinned items
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ if (listAdapter == null) {
+ handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter());
+ if (mChooserMultiProfilePagerAdapter.getCount() > 1) {
+ handlePackageChangePerProfile(
+ mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
+ }
+ } else {
+ handlePackageChangePerProfile(listAdapter);
+ }
+ updateProfileViewButton();
+ }
+
+ private void handlePackageChangePerProfile(ResolverListAdapter adapter) {
+ ProfileRecord record = getProfileRecord(adapter.getUserHandle());
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ adapter.handlePackagesChanged();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+ mFinishWhenStopped = false;
+ mRefinementManager.onActivityResume();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager.isLayoutRtl()) {
+ mMultiProfilePagerAdapter.setupViewPager(viewPager);
+ }
+
+ mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
+ adjustPreviewWidth(newConfig.orientation, null);
+ updateStickyContentPreview();
+ updateTabPadding();
+ }
+
+ private boolean shouldDisplayLandscape(int orientation) {
+ // Sharesheet fixes the # of items per row and therefore can not correctly lay out
+ // when in the restricted size of multi-window mode. In the future, would be nice
+ // to use minimum dp size requirements instead
+ return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
+ }
+
+ private void adjustPreviewWidth(int orientation, View parent) {
+ int width = -1;
+ if (mShouldDisplayLandscape) {
+ width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
+ }
+
+ parent = parent == null ? getWindow().getDecorView() : parent;
+
+ updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
+ }
+
+ private void updateTabPadding() {
+ if (shouldShowTabs()) {
+ View tabs = findViewById(com.android.internal.R.id.tabs);
+ float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
+ // The entire width consists of icons or padding. Divide the item padding in half to get
+ // paddingHorizontal.
+ float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
+ / mMaxTargetsPerRow / 2;
+ // Subtract the margin the buttons already have.
+ padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
+ tabs.setPadding((int) padding, 0, (int) padding, 0);
+ }
+ }
+
+ private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
+ View view = parent.findViewById(layoutResourceId);
+ if (view != null && view.getLayoutParams() != null) {
+ LayoutParams params = view.getLayoutParams();
+ params.width = width;
+ view.setLayoutParams(params);
+ }
+ }
+
+ /**
+ * Create a view that will be shown in the content preview area
+ * @param parent reference to the parent container where the view should be attached to
+ * @return content preview view
+ */
+ protected ViewGroup createContentPreviewView(ViewGroup parent) {
+ ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
+ getResources(),
+ getLayoutInflater(),
+ parent,
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+
+ return layout;
+ }
+
+ @Nullable
+ private View getFirstVisibleImgPreviewView() {
+ View imagePreview = findViewById(R.id.scrollable_image_preview);
+ return imagePreview instanceof ImagePreviewView
+ ? ((ImagePreviewView) imagePreview).getTransitionView()
+ : null;
+ }
+
+ /**
+ * Wrapping the ContentResolver call to expose for easier mocking,
+ * and to avoid mocking Android core classes.
+ */
+ @VisibleForTesting
+ public Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ return resolver.query(uri, null, null, null, null);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mRefinementManager.onActivityStop(isChangingConfigurations());
+
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
+ finish();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (isFinishing()) {
+ mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
+ }
+
+ mBackgroundThreadPoolExecutor.shutdownNow();
+
+ destroyProfileRecords();
+ }
+
+ private void destroyProfileRecords() {
+ for (int i = 0; i < mProfileRecords.size(); ++i) {
+ mProfileRecords.valueAt(i).destroy();
+ }
+ mProfileRecords.clear();
+ }
+
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ if (mChooserRequest == null) {
+ return defIntent;
+ }
+
+ Intent result = defIntent;
+ if (mChooserRequest.getReplacementExtras() != null) {
+ final Bundle replExtras =
+ mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
+ if (replExtras != null) {
+ result = new Intent(defIntent);
+ result.putExtras(replExtras);
+ }
+ }
+ if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
+ || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ result = Intent.createChooser(result,
+ getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
+
+ // Don't auto-launch single intents if the intent is being forwarded. This is done
+ // because automatically launching a resolving application as a response to the user
+ // action of switching accounts is pretty unexpected.
+ result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
+ }
+ return result;
+ }
+
+ @Override
+ public void onActivityStarted(TargetInfo cti) {
+ if (mChooserRequest.getChosenComponentSender() != null) {
+ final ComponentName target = cti.getResolvedComponentName();
+ if (target != null) {
+ final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
+ try {
+ mChooserRequest.getChosenComponentSender().sendIntent(
+ this, Activity.RESULT_OK, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ + "the chosen component: " + e);
+ }
+ }
+ }
+ }
+
+ private void addCallerChooserTargets() {
+ if (!mChooserRequest.getCallerChooserTargets().isEmpty()) {
+ // Send the caller's chooser targets only to the default profile.
+ UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK)
+ ? getAnnotatedUserHandles().workProfileUserHandle
+ : getAnnotatedUserHandles().personalProfileUserHandle;
+ if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
+ /* origTarget */ null,
+ new ArrayList<>(mChooserRequest.getCallerChooserTargets()),
+ TARGET_TYPE_DEFAULT,
+ /* directShareShortcutInfoCache */ Collections.emptyMap(),
+ /* directShareAppTargetCache */ Collections.emptyMap());
+ }
+ }
+ }
+
+ @Override
+ public int getLayoutResource() {
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ // Note that this is only safe because the Intent handled by the ChooserActivity is
+ // guaranteed to contain no extras unknown to the local ClassLoader. That is why this
+ // method can not be replaced in the ResolverActivity whole hog.
+ if (!super.shouldAutoLaunchSingleChoice(target)) {
+ return false;
+ }
+
+ return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
+ }
+
+ private void showTargetDetails(TargetInfo targetInfo) {
+ if (targetInfo == null) return;
+
+ List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets();
+ if (targetList.isEmpty()) {
+ Log.e(TAG, "No displayable data to show target details");
+ return;
+ }
+
+ // TODO: implement these type-conditioned behaviors polymorphically, and consider moving
+ // the logic into `ChooserTargetActionsDialogFragment.show()`.
+ boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
+ IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
+ ? mChooserRequest.getTargetIntentFilter() : null;
+ String shortcutTitle = targetInfo.isSelectableTargetInfo()
+ ? targetInfo.getDisplayLabel().toString() : null;
+ String shortcutIdKey = targetInfo.getDirectShareShortcutId();
+
+ ChooserTargetActionsDialogFragment.show(
+ getSupportFragmentManager(),
+ targetList,
+ // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
+ // resolved correctly within the same tab.
+ targetInfo.getResolveInfo().userHandle,
+ shortcutIdKey,
+ shortcutTitle,
+ isShortcutPinned,
+ intentFilter);
+ }
+
+ @Override
+ protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+ if (mRefinementManager.maybeHandleSelection(
+ target,
+ mChooserRequest.getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ return false;
+ }
+ updateModelAndChooserCounts(target);
+ maybeRemoveSharedText(target);
+ return super.onTargetSelected(target, alwaysCheck);
+ }
+
+ @Override
+ public void startSelected(int which, boolean always, boolean filtered) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ TargetInfo targetInfo = currentListAdapter
+ .targetInfoForPosition(which, filtered);
+ if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
+ return;
+ }
+
+ final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
+
+ if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) {
+ MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
+ if (!mti.hasSelected()) {
+ // Add userHandle based badge to the stackedAppDialogBox.
+ ChooserStackedAppDialogFragment.show(
+ getSupportFragmentManager(),
+ mti,
+ which,
+ targetInfo.getResolveInfo().userHandle);
+ return;
+ }
+ }
+
+ super.startSelected(which, always, filtered);
+
+ // TODO: both of the conditions around this switch logic *should* be redundant, and
+ // can be removed if certain invariants can be guaranteed. In particular, it seems
+ // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably*
+ // expected to be null only at out-of-bounds indexes where `getPositionTargetType()`
+ // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't
+ // need to null-check targetInfo. We only need the null check if it's possible that
+ // the ChooserListAdapter contains null elements "in the middle" of its list data,
+ // such that they're classified as belonging to one of the real target types. That
+ // should probably never happen. But why would this method ever be invoked with a
+ // null target at all? Even an out-of-bounds index should never be "selected"...
+ if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) {
+ switch (currentListAdapter.getPositionTargetType(which)) {
+ case ChooserListAdapter.TARGET_SERVICE:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_SERVICE,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ which,
+ /* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
+ mChooserRequest.getCallerChooserTargets().size(),
+ targetInfo.getHashedTargetIdForMetrics(this),
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_CALLER:
+ case ChooserListAdapter.TARGET_STANDARD:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_APP,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ (which - currentListAdapter.getSurfacedTargetInfo().size()),
+ /* directTargetAlsoRanked= */ -1,
+ currentListAdapter.getCallerTargetCount(),
+ /* directTargetHashed= */ null,
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_STANDARD_AZ:
+ // A-Z targets are unranked standard targets; we use a value of -1 to mark that
+ // they are from the alphabetical pool.
+ // TODO: why do we log a different selection type if the -1 value already
+ // designates the same condition?
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_STANDARD,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ /* value= */ -1,
+ /* directTargetAlsoRanked= */ -1,
+ /* numCallerProvided= */ 0,
+ /* directTargetHashed= */ null,
+ /* isPinned= */ false,
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ }
+ }
+ }
+
+ private int getRankedPosition(TargetInfo targetInfo) {
+ String targetPackageName =
+ targetInfo.getChooserTargetComponentName().getPackageName();
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ int maxRankedResults = Math.min(
+ currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
+
+ for (int i = 0; i < maxRankedResults; i++) {
+ if (currentListAdapter.getDisplayResolveInfo(i)
+ .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected boolean shouldAddFooterView() {
+ // To accommodate for window insets
+ return true;
+ }
+
+ @Override
+ protected void applyFooterView(int height) {
+ int count = mChooserMultiProfilePagerAdapter.getItemCount();
+
+ for (int i = 0; i < count; i++) {
+ mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height);
+ }
+ }
+
+ private void logDirectShareTargetReceived(UserHandle forUser) {
+ ProfileRecord profileRecord = getProfileRecord(forUser);
+ if (profileRecord == null) {
+ return;
+ }
+ getEventLog().logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+ (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
+ }
+
+ void updateModelAndChooserCounts(TargetInfo info) {
+ if (info != null && info.isMultiDisplayResolveInfo()) {
+ info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
+ }
+ if (info != null) {
+ sendClickToAppPredictor(info);
+ final ResolveInfo ri = info.getResolveInfo();
+ Intent targetIntent = getTargetIntent();
+ if (ri != null && ri.activityInfo != null && targetIntent != null) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ if (currentListAdapter != null) {
+ sendImpressionToAppPredictor(info, currentListAdapter);
+ currentListAdapter.updateModel(info);
+ currentListAdapter.updateChooserCounts(
+ ri.activityInfo.packageName,
+ targetIntent.getAction(),
+ ri.userHandle);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
+ Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
+ }
+ }
+ mIsSuccessfullySelected = true;
+ }
+
+ private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {
+ Intent targetIntent = targetInfo.getTargetIntent();
+ if (targetIntent == null) {
+ return;
+ }
+ Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent());
+ // Our TargetInfo implementations add associated component to the intent, let's do the same
+ // for the sake of the comparison below.
+ if (targetIntent.getComponent() != null) {
+ originalTargetIntent.setComponent(targetIntent.getComponent());
+ }
+ // Use filterEquals as a way to check that the primary intent is in use (and not an
+ // alternative one). For example, an app is sharing an image and a link with mime type
+ // "image/png" and provides an alternative intent to share only the link with mime type
+ // "text/uri". Should there be a target that accepts only the latter, the alternative intent
+ // will be used and we don't want to exclude the link from it.
+ if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) {
+ targetIntent.removeExtra(Intent.EXTRA_TEXT);
+ }
+ }
+
+ private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+ List<AppTargetId> targetIds = new ArrayList<>();
+ for (TargetInfo chooserTargetInfo : surfacedTargetInfo) {
+ ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
+ if (shortcutInfo != null) {
+ ComponentName componentName =
+ chooserTargetInfo.getChooserTargetComponentName();
+ targetIds.add(new AppTargetId(
+ String.format(
+ "%s/%s/%s",
+ shortcutInfo.getId(),
+ componentName.flattenToString(),
+ SHORTCUT_TARGET)));
+ }
+ }
+ directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds);
+ }
+
+ private void sendClickToAppPredictor(TargetInfo targetInfo) {
+ if (!targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ AppTarget appTarget = targetInfo.getDirectShareAppTarget();
+ if (appTarget != null) {
+ // This is a direct share click that was provided by the APS
+ directShareAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE)
+ .build());
+ }
+ }
+
+ @Nullable
+ private AppPredictor getAppPredictor(UserHandle userHandle) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ // We cannot use APS service when clone profile is present as APS service cannot sort
+ // cross profile targets as of now.
+ return ((record == null) || (getAnnotatedUserHandles().cloneProfileUserHandle != null))
+ ? null : record.appPredictor;
+ }
+
+ /**
+ * Sort intents alphabetically based on display label.
+ */
+ static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
+ Comparator<DisplayResolveInfo> mComparator;
+ AzInfoComparator(Context context) {
+ Collator collator = Collator
+ .getInstance(context.getResources().getConfiguration().locale);
+ // Adding two stage comparator, first stage compares using displayLabel, next stage
+ // compares using resolveInfo.userHandle
+ mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator)
+ .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier());
+ }
+
+ @Override
+ public int compare(
+ DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
+ return mComparator.compare(lhsp, rhsp);
+ }
+ }
+
+ protected EventLog getEventLog() {
+ return mEventLog;
+ }
+
+ public class ChooserListController extends ResolverListController {
+ public ChooserListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackageName,
+ int launchedFromUid,
+ AbstractResolverComparator resolverComparator,
+ UserHandle queryIntentsAsUser) {
+ super(
+ context,
+ pm,
+ targetIntent,
+ referrerPackageName,
+ launchedFromUid,
+ resolverComparator,
+ queryIntentsAsUser);
+ }
+
+ @Override
+ public boolean isComponentFiltered(ComponentName name) {
+ return mChooserRequest.getFilteredComponentNames().contains(name);
+ }
+
+ @Override
+ public boolean isComponentPinned(ComponentName name) {
+ return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+ }
+ }
+
+ @VisibleForTesting
+ public ChooserGridAdapter createChooserGridAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ ChooserListAdapter chooserListAdapter = createChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ getTargetIntent(),
+ mChooserRequest,
+ mMaxTargetsPerRow,
+ targetDataLoader);
+
+ return new ChooserGridAdapter(
+ context,
+ new ChooserGridAdapter.ChooserActivityDelegate() {
+ @Override
+ public boolean shouldShowTabs() {
+ return ChooserActivity.this.shouldShowTabs();
+ }
+
+ @Override
+ public View buildContentPreview(ViewGroup parent) {
+ return createContentPreviewView(parent);
+ }
+
+ @Override
+ public void onTargetSelected(int itemIndex) {
+ startSelected(itemIndex, false, true);
+ }
+
+ @Override
+ public void onTargetLongPressed(int selectedPosition) {
+ final TargetInfo longPressedTargetInfo =
+ mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter()
+ .targetInfoForPosition(
+ selectedPosition, /* filtered= */ true);
+ // Only a direct share target or an app target is expected
+ if (longPressedTargetInfo.isDisplayResolveInfo()
+ || longPressedTargetInfo.isSelectableTargetInfo()) {
+ showTargetDetails(longPressedTargetInfo);
+ }
+ }
+
+ @Override
+ public void updateProfileViewButton(View newButtonFromProfileRow) {
+ mProfileView = newButtonFromProfileRow;
+ mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+ ChooserActivity.this.updateProfileViewButton();
+ }
+ },
+ chooserListAdapter,
+ shouldShowContentPreview(),
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ @VisibleForTesting
+ 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) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ targetIntent,
+ this,
+ context.getPackageManager(),
+ getEventLog(),
+ chooserRequest,
+ maxTargetsPerRow,
+ initialIntentsUserSpace,
+ targetDataLoader);
+ }
+
+ @Override
+ protected void onWorkProfileStatusUpdated() {
+ UserHandle workUser = getAnnotatedUserHandles().workProfileUserHandle;
+ ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ super.onWorkProfileStatusUpdated();
+ }
+
+ @Override
+ @VisibleForTesting
+ protected ChooserListController createListController(UserHandle userHandle) {
+ AppPredictor appPredictor = getAppPredictor(userHandle);
+ AbstractResolverComparator resolverComparator;
+ if (appPredictor != null) {
+ resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
+ getReferrerPackageName(), appPredictor, userHandle, getEventLog(),
+ getIntegratedDeviceComponents().getNearbySharingComponent());
+ } else {
+ resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ null,
+ getEventLog(),
+ getResolverRankerServiceUserHandleList(userHandle),
+ getIntegratedDeviceComponents().getNearbySharingComponent());
+ }
+
+ return new ChooserListController(
+ this,
+ mPm,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ getAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ @VisibleForTesting
+ protected ViewModelProvider.Factory createPreviewViewModelFactory() {
+ return PreviewViewModel.Companion.getFactory();
+ }
+
+ private ChooserActionFactory createChooserActionFactory() {
+ return new ChooserActionFactory(
+ this,
+ mChooserRequest,
+ mIntegratedDeviceComponents,
+ getEventLog(),
+ (isExcluded) -> mExcludeSharedText = isExcluded,
+ this::getFirstVisibleImgPreviewView,
+ new ChooserActionFactory.ActionActivityStarter() {
+ @Override
+ public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+ safelyStartActivityAsUser(
+ targetInfo, getAnnotatedUserHandles().personalProfileUserHandle);
+ finish();
+ }
+
+ @Override
+ public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ ChooserActivity.this, sharedElement, sharedElementName);
+ safelyStartActivityAsUser(
+ targetInfo,
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ options.toBundle());
+ // Can't finish right away because the shared element transition may not
+ // be ready to start.
+ mFinishWhenStopped = true;
+ }
+ },
+ (status) -> {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
+ });
+ }
+
+ /*
+ * Need to dynamically adjust how many icons can fit per row before we add them,
+ * which also means setting the correct offset to initially show the content
+ * preview area + 2 rows of targets
+ */
+ private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mChooserMultiProfilePagerAdapter == null) {
+ return;
+ }
+ RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+ // Skip height calculation if recycler view was scrolled to prevent it inaccurately
+ // calculating the height, as the logic below does not account for the scrolled offset.
+ if (gridAdapter == null || recyclerView == null
+ || recyclerView.computeVerticalScrollOffset() != 0) {
+ return;
+ }
+
+ final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
+ boolean isLayoutUpdated =
+ gridAdapter.calculateChooserTargetWidth(availableWidth)
+ || recyclerView.getAdapter() == null
+ || availableWidth != mCurrAvailableWidth;
+
+ boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
+
+ if (isLayoutUpdated
+ || insetsChanged
+ || mLastNumberOfChildren != recyclerView.getChildCount()) {
+ mCurrAvailableWidth = availableWidth;
+ if (isLayoutUpdated) {
+ // It is very important we call setAdapter from here. Otherwise in some cases
+ // the resolver list doesn't get populated, such as b/150922090, b/150918223
+ // and b/150936654
+ recyclerView.setAdapter(gridAdapter);
+ ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount(
+ mMaxTargetsPerRow);
+
+ updateTabPadding();
+ }
+
+ UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
+ int currentProfile = getProfileForUser(currentUserHandle);
+ int initialProfile = findSelectedProfile();
+ if (currentProfile != initialProfile) {
+ return;
+ }
+
+ if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
+ return;
+ }
+
+ getMainThreadHandler().post(() -> {
+ if (mResolverDrawerLayout == null || gridAdapter == null) {
+ return;
+ }
+ int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
+ mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
+ mEnterTransitionAnimationDelegate.markOffsetCalculated();
+ mLastAppliedInsets = mSystemWindowInsets;
+ });
+ }
+ }
+
+ private int calculateDrawerOffset(
+ int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
+
+ int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ int rowsToShow = gridAdapter.getSystemRowCount()
+ + gridAdapter.getProfileRowCount()
+ + gridAdapter.getServiceTargetRowCount()
+ + gridAdapter.getCallerAndRankedTargetRowCount();
+
+ // then this is most likely not a SEND_* action, so check
+ // the app target count
+ if (rowsToShow == 0) {
+ rowsToShow = gridAdapter.getRowCount();
+ }
+
+ // still zero? then use a default height and leave, which
+ // can happen when there are no targets to show
+ if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
+ offset += getResources().getDimensionPixelSize(
+ R.dimen.chooser_max_collapsed_height);
+ return offset;
+ }
+
+ View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container);
+ if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) {
+ offset += stickyContentPreview.getHeight();
+ }
+
+ if (shouldShowTabs()) {
+ offset += findViewById(com.android.internal.R.id.tabs).getHeight();
+ }
+
+ if (recyclerView.getVisibility() == View.VISIBLE) {
+ rowsToShow = Math.min(4, rowsToShow);
+ boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
+ mLastNumberOfChildren = recyclerView.getChildCount();
+ for (int i = 0, childCount = recyclerView.getChildCount();
+ i < childCount && rowsToShow > 0; i++) {
+ View child = recyclerView.getChildAt(i);
+ if (((GridLayoutManager.LayoutParams)
+ child.getLayoutParams()).getSpanIndex() != 0) {
+ continue;
+ }
+ int height = child.getHeight();
+ offset += height;
+ if (shouldShowExtraRow) {
+ offset += height;
+ }
+ rowsToShow--;
+ }
+ } else {
+ ViewGroup currentEmptyStateView = getActiveEmptyStateView();
+ if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
+ offset += currentEmptyStateView.getHeight();
+ }
+ }
+
+ return Math.min(offset, bottom - top);
+ }
+
+ /**
+ * If we have a tabbed view and are showing 1 row in the current profile and an empty
+ * state screen in the other profile, to prevent cropping of the empty state screen we show
+ * a second row in the current profile.
+ */
+ private boolean shouldShowExtraRow(int rowsToShow) {
+ return shouldShowTabs()
+ && rowsToShow == 1
+ && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(
+ mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
+ }
+
+ /**
+ * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle.
+ * Returns {@link #PROFILE_PERSONAL}, otherwise.
+ **/
+ private int getProfileForUser(UserHandle currentUserHandle) {
+ if (currentUserHandle.equals(getAnnotatedUserHandles().workProfileUserHandle)) {
+ return PROFILE_WORK;
+ }
+ // We return personal profile, as it is the default when there is no work profile, personal
+ // profile represents rootUser, clonedUser & secondaryUser, covering all use cases.
+ return PROFILE_PERSONAL;
+ }
+
+ private ViewGroup getActiveEmptyStateView() {
+ int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
+ return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage);
+ }
+
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
+ super.onHandlePackagesChanged(listAdapter);
+ }
+
+ @Override
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ setupScrollListener();
+ maybeSetupGlobalLayoutListener();
+
+ ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
+ UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle();
+ if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
+ mChooserMultiProfilePagerAdapter
+ .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
+ }
+
+ //TODO: move this block inside ChooserListAdapter (should be called when
+ // ResolverListAdapter#mPostListReadyRunnable is executed.
+ if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
+ chooserListAdapter.notifyDataSetChanged();
+ } else {
+ chooserListAdapter.updateAlphabeticalList();
+ }
+
+ if (rebuildComplete) {
+ long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
+ if (duration >= 0) {
+ Log.d(TAG, "app target loading time " + duration + " ms");
+ }
+ addCallerChooserTargets();
+ getEventLog().logSharesheetAppLoadComplete();
+ maybeQueryAdditionalPostProcessingTargets(
+ listProfileUserHandle,
+ chooserListAdapter.getDisplayResolveInfos());
+ mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
+ }
+ }
+
+ private void maybeQueryAdditionalPostProcessingTargets(
+ UserHandle userHandle,
+ DisplayResolveInfo[] displayResolveInfos) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record == null || record.shortcutLoader == null) {
+ return;
+ }
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+ record.shortcutLoader.updateAppTargets(displayResolveInfos);
+ }
+
+ @MainThread
+ private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) {
+ if (DEBUG) {
+ Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
+ }
+ mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache());
+ mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache());
+ ChooserListAdapter adapter =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+ if (adapter != null) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) {
+ adapter.addServiceResults(
+ resultInfo.getAppTarget(),
+ resultInfo.getShortcuts(),
+ result.isFromAppPredictor()
+ ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ mDirectShareShortcutInfoCache,
+ mDirectShareAppTargetCache);
+ }
+ adapter.completeServiceTargetLoading();
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {
+ long duration = Tracer.INSTANCE.endLaunchToShortcutTrace();
+ if (duration >= 0) {
+ Log.d(TAG, "stat to first shortcut time: " + duration + " ms");
+ }
+ }
+ logDirectShareTargetReceived(userHandle);
+ sendVoiceChoicesIfNeeded();
+ getEventLog().logSharesheetDirectLoadComplete();
+ }
+
+ private void setupScrollListener() {
+ if (mResolverDrawerLayout == null) {
+ return;
+ }
+ int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
+ final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);
+ final float defaultElevation = elevatedView.getElevation();
+ final float chooserHeaderScrollElevation =
+ getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setHorizontalScrollingEnabled(true);
+ }
+ } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL;
+ setHorizontalScrollingEnabled(false);
+ }
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView view, int dx, int dy) {
+ if (view.getChildCount() > 0) {
+ View child = view.getLayoutManager().findViewByPosition(0);
+ if (child == null || child.getTop() < 0) {
+ elevatedView.setElevation(chooserHeaderScrollElevation);
+ return;
+ }
+ }
+
+ elevatedView.setElevation(defaultElevation);
+ }
+ });
+ }
+
+ private void maybeSetupGlobalLayoutListener() {
+ if (shouldShowTabs()) {
+ return;
+ }
+ final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ recyclerView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // Fixes an issue were the accessibility border disappears on list creation.
+ recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setFocusable(true);
+ titleView.setFocusableInTouchMode(true);
+ titleView.requestFocus();
+ titleView.requestAccessibilityFocus();
+ }
+ }
+ });
+ }
+
+ /**
+ * The sticky content preview is shown only when we have a tabbed view. It's shown above
+ * the tabs so it is not part of the scrollable list. If we are not in tabbed view,
+ * we instead show the content preview as a regular list item.
+ */
+ private boolean shouldShowStickyContentPreview() {
+ return shouldShowStickyContentPreviewNoOrientationCheck();
+ }
+
+ private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
+ }
+
+ /**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
+ * @return true if we want to show the content preview area
+ */
+ protected boolean shouldShowContentPreview() {
+ return (mChooserRequest != null) && mChooserRequest.isSendActionTarget();
+ }
+
+ private void updateStickyContentPreview() {
+ if (shouldShowStickyContentPreviewNoOrientationCheck()) {
+ // The sticky content preview is only shown when we show the work and personal tabs.
+ // We don't show it in landscape as otherwise there is no room for scrolling.
+ // If the sticky content preview will be shown at some point with orientation change,
+ // then always preload it to avoid subsequent resizing of the share sheet.
+ ViewGroup contentPreviewContainer =
+ findViewById(com.android.internal.R.id.content_preview_container);
+ if (contentPreviewContainer.getChildCount() == 0) {
+ ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
+ contentPreviewContainer.addView(contentPreviewView);
+ }
+ }
+ if (shouldShowStickyContentPreview()) {
+ showStickyContentPreview();
+ } else {
+ hideStickyContentPreview();
+ }
+ }
+
+ private void showStickyContentPreview() {
+ if (isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean isStickyContentPreviewShowing() {
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ return contentPreviewContainer.getVisibility() == View.VISIBLE;
+ }
+
+ private void hideStickyContentPreview() {
+ if (!isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.GONE);
+ }
+
+ private View findRootView() {
+ if (mContentView == null) {
+ mContentView = findViewById(android.R.id.content);
+ }
+ return mContentView;
+ }
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ public void onButtonClick(View v) {}
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ protected void resetButtonBar() {}
+
+ @Override
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_CHOOSER;
+ }
+
+ @Override
+ protected void onProfileTabSelected() {
+ // This fixes an edge case where after performing a variety of gestures, vertical scrolling
+ // ends up disabled. That's because at some point the old tab's vertical scrolling is
+ // disabled and the new tab's is enabled. For context, see b/159997845
+ setVerticalScrollEnabled(true);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.scrollNestedScrollableChildBackToTop();
+ }
+ }
+
+ @Override
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter
+ .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
+ mChooserMultiProfilePagerAdapter.setupContainerPadding(
+ getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container));
+ }
+
+ WindowInsets result = super.onApplyWindowInsets(v, insets);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.requestLayout();
+ }
+ return result;
+ }
+
+ private void setHorizontalScrollingEnabled(boolean enabled) {
+ ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSwipingEnabled(enabled);
+ }
+
+ private void setVerticalScrollEnabled(boolean enabled) {
+ ChooserGridLayoutManager layoutManager =
+ (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .getLayoutManager();
+ layoutManager.setVerticalScrollEnabled(enabled);
+ }
+
+ @Override
+ void onHorizontalSwipeStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL;
+ setVerticalScrollEnabled(false);
+ }
+ } else if (state == ViewPager.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setVerticalScrollEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void maybeLogProfileChange() {
+ getEventLog().logSharesheetProfileChanged();
+ }
+
+ private static class ProfileRecord {
+ /** The {@link AppPredictor} for this profile, if any. */
+ @Nullable
+ public final AppPredictor appPredictor;
+ /**
+ * null if we should not load shortcuts.
+ */
+ @Nullable
+ public final ShortcutLoader shortcutLoader;
+ public long loadingStartTime;
+
+ private ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
+ this.appPredictor = appPredictor;
+ this.shortcutLoader = shortcutLoader;
+ }
+
+ public void destroy() {
+ if (appPredictor != null) {
+ appPredictor.destroy();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
new file mode 100644
index 00000000..378bc06c
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
@@ -0,0 +1,36 @@
+package com.android.intentresolver.v2
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.intentresolver.FeatureFlags
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint(BroadcastReceiver::class)
+class ChooserSelector : Hilt_ChooserSelector() {
+
+ @Inject lateinit var featureFlags: FeatureFlags
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS),
+ if (featureFlags.modularFramework()) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ },
+ /* flags = */ 0,
+ )
+ }
+ }
+
+ companion object {
+ private const val CHOOSER_PACKAGE = "com.android.intentresolver"
+ private const val CHOOSER_CLASS = ".v2.ChooserActivity"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java
new file mode 100644
index 00000000..dd6842aa
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java
@@ -0,0 +1,2426 @@
+/*
+ * 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 static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
+
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.UiThread;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Space;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.MultiProfilePagerAdapter;
+import com.android.intentresolver.MultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.MultiProfilePagerAdapter.Profile;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.ResolverMultiProfilePagerAdapter;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider;
+import com.android.intentresolver.icons.DefaultTargetDataLoader;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.util.LatencyTracker;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
+ * *not* the resolver that is actually triggered by the system right now (you want
+ * frameworks/base/core/java/com/android/internal/app/ResolverActivity.java for that), the full
+ * migration is not complete.
+ */
+@UiThread
+public class ResolverActivity extends FragmentActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+
+ public ResolverActivity() {
+ mIsIntentPicker = getClass().equals(ResolverActivity.class);
+ }
+
+ protected ResolverActivity(boolean isIntentPicker) {
+ mIsIntentPicker = isIntentPicker;
+ }
+
+ /**
+ * Whether to enable a launch mode that is safe to use when forwarding intents received from
+ * applications and running in system processes. This mode uses Activity.startActivityAsCaller
+ * instead of the normal Activity.startActivity for launching the activity selected
+ * by the user.
+ */
+ private boolean mSafeForwardingMode;
+
+ private Button mAlwaysButton;
+ private Button mOnceButton;
+ protected View mProfileView;
+ private int mLastSelected = AbsListView.INVALID_POSITION;
+ private boolean mResolvingHome = false;
+ private String mProfileSwitchMessage;
+ private int mLayoutId;
+ @VisibleForTesting
+ protected final ArrayList<Intent> mIntents = new ArrayList<>();
+ private PickTargetOptionRequest mPickOptionRequest;
+ private String mReferrerPackage;
+ private CharSequence mTitle;
+ private int mDefaultTitleResId;
+ // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
+ private final boolean mIsIntentPicker;
+
+ // Whether or not this activity supports choosing a default handler for the intent.
+ @VisibleForTesting
+ protected boolean mSupportsAlwaysUseOption;
+ protected ResolverDrawerLayout mResolverDrawerLayout;
+ protected PackageManager mPm;
+
+ private static final String TAG = "ResolverActivity";
+ private static final boolean DEBUG = false;
+ private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
+
+ private boolean mRegistered;
+
+ protected Insets mSystemWindowInsets = null;
+ private Space mFooterSpacer = null;
+
+ /** See {@link #setRetainInOnStop}. */
+ private boolean mRetainInOnStop;
+
+ protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
+ protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+
+ /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
+ private boolean mWorkProfileHasBeenEnabled = false;
+
+ private static final String TAB_TAG_PERSONAL = "personal";
+ private static final String TAB_TAG_WORK = "work";
+
+ private PackageMonitor mPersonalPackageMonitor;
+ private PackageMonitor mWorkPackageMonitor;
+
+ private TargetDataLoader mTargetDataLoader;
+
+ @VisibleForTesting
+ protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
+
+ protected WorkProfileAvailabilityManager mWorkProfileAvailability;
+
+ // Intent extra for connected audio devices
+ public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
+
+ /**
+ * Integer extra to indicate which profile should be automatically selected.
+ * <p>Can only be used if there is a work profile.
+ * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
+ */
+ protected static final String EXTRA_SELECTED_PROFILE =
+ "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
+
+ /**
+ * {@link UserHandle} extra to indicate the user of the user that the starting intent
+ * originated from.
+ * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()},
+ * as there are edge cases when the intent resolver is launched in the other profile.
+ * For example, when we have 0 resolved apps in current profile and multiple resolved
+ * apps in the other profile, opening a link from the current profile launches the intent
+ * resolver in the other one. b/148536209 for more info.
+ */
+ static final String EXTRA_CALLING_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
+
+ protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
+
+ private UserHandle mHeaderCreatorUser;
+
+ // User handle annotations are lazy-initialized to ensure that they're computed exactly once
+ // (even though they can't be computed prior to activity creation).
+ // TODO: use a less ad-hoc pattern for lazy initialization (by switching to Dagger or
+ // introducing a common `LazySingletonSupplier` API, etc), and/or migrate all dependents to a
+ // new component whose lifecycle is limited to the "created" Activity (so that we can just hold
+ // the annotations as a `final` ivar, which is a better way to show immutability).
+ private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> {
+ final AnnotatedUserHandles result = computeAnnotatedUserHandles();
+ mLazyAnnotatedUserHandles = () -> result;
+ return result;
+ };
+
+ // This method is called exactly once during creation to compute the immutable annotations
+ // accessible through the lazy supplier {@link mLazyAnnotatedUserHandles}.
+ // TODO: this is only defined so that tests can provide an override that injects fake
+ // annotations. Dagger could provide a cleaner model for our testing/injection requirements.
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ protected AnnotatedUserHandles computeAnnotatedUserHandles() {
+ return AnnotatedUserHandles.forShareActivity(this);
+ }
+
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
+ protected final LatencyTracker mLatencyTracker = getLatencyTracker();
+
+ private enum ActionTitle {
+ VIEW(Intent.ACTION_VIEW,
+ R.string.whichViewApplication,
+ R.string.whichViewApplicationNamed,
+ R.string.whichViewApplicationLabel),
+ EDIT(Intent.ACTION_EDIT,
+ R.string.whichEditApplication,
+ R.string.whichEditApplicationNamed,
+ R.string.whichEditApplicationLabel),
+ SEND(Intent.ACTION_SEND,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ SENDTO(Intent.ACTION_SENDTO,
+ R.string.whichSendToApplication,
+ R.string.whichSendToApplicationNamed,
+ R.string.whichSendToApplicationLabel),
+ SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
+ R.string.whichImageCaptureApplication,
+ R.string.whichImageCaptureApplicationNamed,
+ R.string.whichImageCaptureApplicationLabel),
+ DEFAULT(null,
+ R.string.whichApplication,
+ R.string.whichApplicationNamed,
+ R.string.whichApplicationLabel),
+ HOME(Intent.ACTION_MAIN,
+ R.string.whichHomeApplication,
+ R.string.whichHomeApplicationNamed,
+ R.string.whichHomeApplicationLabel);
+
+ // titles for layout that deals with http(s) intents
+ public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith;
+ public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith;
+ public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp;
+ public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp;
+
+ public final String action;
+ public final int titleRes;
+ public final int namedTitleRes;
+ public final @StringRes int labelRes;
+
+ ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
+ this.action = action;
+ this.titleRes = titleRes;
+ this.namedTitleRes = namedTitleRes;
+ this.labelRes = labelRes;
+ }
+
+ public static ActionTitle forAction(String action) {
+ for (ActionTitle title : values()) {
+ if (title != HOME && action != null && action.equals(title.action)) {
+ return title;
+ }
+ }
+ return DEFAULT;
+ }
+ }
+
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ listAdapter.handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // We care about all package changes, not just the whole package itself which is
+ // default behavior.
+ return true;
+ }
+ };
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ // Use a specialized prompt when we're handling the 'Home' app startActivity()
+ final Intent intent = makeMyIntent();
+ final Set<String> categories = intent.getCategories();
+ if (Intent.ACTION_MAIN.equals(intent.getAction())
+ && categories != null
+ && categories.size() == 1
+ && categories.contains(Intent.CATEGORY_HOME)) {
+ // Note: this field is not set to true in the compatibility version.
+ mResolvingHome = true;
+ }
+
+ onCreate(
+ savedInstanceState,
+ intent,
+ /* additionalTargets= */ null,
+ /* title= */ null,
+ /* defaultTitleRes= */ 0,
+ /* initialIntents= */ null,
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ true,
+ createIconLoader(),
+ /* safeForwardingMode= */ true);
+ }
+
+ /**
+ * Compatibility version for other bundled services that use this overload without
+ * a default title resource
+ */
+ protected void onCreate(
+ Bundle savedInstanceState,
+ Intent intent,
+ CharSequence title,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean supportsAlwaysUseOption,
+ boolean safeForwardingMode) {
+ onCreate(
+ savedInstanceState,
+ intent,
+ null,
+ title,
+ 0,
+ initialIntents,
+ resolutionList,
+ supportsAlwaysUseOption,
+ createIconLoader(),
+ safeForwardingMode);
+ }
+
+ protected void onCreate(
+ Bundle savedInstanceState,
+ Intent intent,
+ Intent[] additionalTargets,
+ CharSequence title,
+ int defaultTitleRes,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean supportsAlwaysUseOption,
+ TargetDataLoader targetDataLoader,
+ boolean safeForwardingMode) {
+ setTheme(appliedThemeResId());
+ super.onCreate(savedInstanceState);
+
+ // Determine whether we should show that intent is forwarded
+ // from managed profile to owner or other way around.
+ setProfileSwitchMessage(intent.getContentUserHint());
+
+ // Force computation of user handle annotations in order to validate the caller ID. (See the
+ // associated TODO comment to explain why this is structured as a lazy computation.)
+ AnnotatedUserHandles unusedReferenceToHandles = mLazyAnnotatedUserHandles.get();
+
+ mWorkProfileAvailability = createWorkProfileAvailabilityManager();
+
+ mPm = getPackageManager();
+
+ mReferrerPackage = getReferrerPackageName();
+
+ // The initial intent must come before any other targets that are to be added.
+ mIntents.add(0, new Intent(intent));
+ if (additionalTargets != null) {
+ Collections.addAll(mIntents, additionalTargets);
+ }
+
+ mTitle = title;
+ mDefaultTitleResId = defaultTitleRes;
+
+ mSupportsAlwaysUseOption = supportsAlwaysUseOption;
+ mSafeForwardingMode = safeForwardingMode;
+ mTargetDataLoader = targetDataLoader;
+
+ // The last argument of createResolverListAdapter is whether to do special handling
+ // of the last used choice to highlight it in the list. We need to always
+ // turn this off when running under voice interaction, since it results in
+ // a more complicated UI that the current voice interaction flow is not able
+ // to handle. We also turn it off when the work tab is shown to simplify the UX.
+ // We also turn it off when clonedProfile is present on the device, because we might have
+ // different "last chosen" activities in the different profiles, and PackageManager doesn't
+ // provide any more information to help us select between them.
+ boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction()
+ && !shouldShowTabs() && !hasCloneProfile();
+ mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ if (configureContentView(targetDataLoader)) {
+ return;
+ }
+
+ mPersonalPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this, getMainLooper(), getAnnotatedUserHandles().personalProfileUserHandle, false);
+ if (shouldShowTabs()) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(
+ this, getMainLooper(), getAnnotatedUserHandles().workProfileUserHandle, false);
+ }
+
+ mRegistered = true;
+
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
+ @Override
+ public void onDismissed() {
+ finish();
+ }
+ });
+
+ boolean hasTouchScreen = getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
+
+ if (isVoiceInteraction() || !hasTouchScreen) {
+ rdl.setCollapsed(false);
+ }
+
+ rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
+
+ mResolverDrawerLayout = rdl;
+ }
+
+ mProfileView = findViewById(com.android.internal.R.id.profile_button);
+ if (mProfileView != null) {
+ mProfileView.setOnClickListener(this::onProfileClick);
+ updateProfileViewButton();
+ }
+
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray()) : ""));
+ }
+
+ protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ if (shouldShowTabs()) {
+ resolverMultiProfilePagerAdapter =
+ createResolverMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ } else {
+ resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ }
+ return resolverMultiProfilePagerAdapter;
+ }
+
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser());
+
+ if (!shouldShowNoCrossProfileIntentsEmptyState) {
+ // Implementation that doesn't show any blockers
+ return new EmptyStateProvider() {};
+ }
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ protected int appliedThemeResId() {
+ return R.style.Theme_DeviceDefault_Resolver;
+ }
+
+ /**
+ * Numerous layouts are supported, each with optional ViewGroups.
+ * Make sure the inset gets added to the correct View, using
+ * a footer for Lists so it can properly scroll under the navbar.
+ */
+ protected boolean shouldAddFooterView() {
+ if (useLayoutWithDefault()) return true;
+
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true;
+
+ return false;
+ }
+
+ protected void applyFooterView(int height) {
+ if (mFooterSpacer == null) {
+ mFooterSpacer = new Space(getApplicationContext());
+ } else {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().removeFooterView(mFooterSpacer);
+ }
+ mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
+ mSystemWindowInsets.bottom));
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().addFooterView(mFooterSpacer);
+ }
+
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ mSystemWindowInsets = insets.getSystemWindowInsets();
+
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+
+ resetButtonBar();
+
+ if (shouldUseMiniResolver()) {
+ View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+ + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing));
+ }
+
+ // Need extra padding so the list can fully scroll up
+ if (shouldAddFooterView()) {
+ applyFooterView(mSystemWindowInsets.bottom);
+ }
+
+ return insets.consumeSystemWindowInsets();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+ && !shouldUseMiniResolver()) {
+ updateIntentPickerPaddings();
+ }
+
+ if (mSystemWindowInsets != null) {
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+ }
+ }
+
+ public int getLayoutResource() {
+ return R.layout.resolver_list;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ final Window window = this.getWindow();
+ final WindowManager.LayoutParams attrs = window.getAttributes();
+ attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ window.setAttributes(attrs);
+
+ if (mRegistered) {
+ mPersonalPackageMonitor.unregister();
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ final Intent intent = getIntent();
+ if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+ && !mResolvingHome && !mRetainInOnStop) {
+ // This resolver is in the unusual situation where it has been
+ // launched at the top of a new task. We don't let it be added
+ // to the recent tasks shown to the user, and we need to make sure
+ // that each time we are launched we get the correct launching
+ // uid (not re-using the same resolver from an old launching uid),
+ // so we will now finish ourself since being no longer visible,
+ // the user probably can't get back to us.
+ if (!isChangingConfigurations()) {
+ finish();
+ }
+ }
+ // TODO: should we clean up the work-profile manager before we potentially finish() above?
+ mWorkProfileAvailability.unregisterWorkProfileStateReceiver(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mMultiProfilePagerAdapter != null
+ && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+ mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ }
+ }
+
+ public void onButtonClick(View v) {
+ final int id = v.getId();
+ ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int which = currentListAdapter.hasFilteredItem()
+ ? currentListAdapter.getFilteredPosition()
+ : listView.getCheckedItemPosition();
+ boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem();
+ startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered);
+ }
+
+ public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
+ if (isFinishing()) {
+ return;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(which, hasIndexBeenFiltered);
+ if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ Toast.makeText(this,
+ getWorkProfileNotSupportedMsg(
+ ri.activityInfo.loadLabel(getPackageManager()).toString()),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(which, hasIndexBeenFiltered);
+ if (target == null) {
+ return;
+ }
+ if (onTargetSelected(target, always)) {
+ if (always && mSupportsAlwaysUseOption) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+ } else if (mSupportsAlwaysUseOption) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+ } else {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ }
+ MetricsLogger.action(this,
+ mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+ finish();
+ }
+ }
+
+ /**
+ * Replace me in subclasses!
+ */
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ return defIntent;
+ }
+
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
+ final ItemClickListener listener = new ItemClickListener();
+ setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
+ if (shouldShowTabs() && mIsIntentPicker) {
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setMaxCollapsedHeight(getResources()
+ .getDimensionPixelSize(useLayoutWithDefault()
+ ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs
+ : R.dimen.resolver_max_collapsed_height_with_tabs));
+ }
+ }
+ }
+
+ protected boolean onTargetSelected(TargetInfo target, boolean always) {
+ final ResolveInfo ri = target.getResolveInfo();
+ final Intent intent = target != null ? target.getResolvedIntent() : null;
+
+ if (intent != null && (mSupportsAlwaysUseOption
+ || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
+ && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
+ // Build a reasonable intent filter, based on what matched.
+ IntentFilter filter = new IntentFilter();
+ Intent filterIntent;
+
+ if (intent.getSelector() != null) {
+ filterIntent = intent.getSelector();
+ } else {
+ filterIntent = intent;
+ }
+
+ String action = filterIntent.getAction();
+ if (action != null) {
+ filter.addAction(action);
+ }
+ Set<String> categories = filterIntent.getCategories();
+ if (categories != null) {
+ for (String cat : categories) {
+ filter.addCategory(cat);
+ }
+ }
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+
+ int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
+ Uri data = filterIntent.getData();
+ if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
+ String mimeType = filterIntent.resolveType(this);
+ if (mimeType != null) {
+ try {
+ filter.addDataType(mimeType);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w("ResolverActivity", e);
+ filter = null;
+ }
+ }
+ }
+ if (data != null && data.getScheme() != null) {
+ // We need the data specification if there was no type,
+ // OR if the scheme is not one of our magical "file:"
+ // or "content:" schemes (see IntentFilter for the reason).
+ if (cat != IntentFilter.MATCH_CATEGORY_TYPE
+ || (!"file".equals(data.getScheme())
+ && !"content".equals(data.getScheme()))) {
+ filter.addDataScheme(data.getScheme());
+
+ // Look through the resolved filter to determine which part
+ // of it matched the original Intent.
+ Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
+ if (pIt != null) {
+ String ssp = data.getSchemeSpecificPart();
+ while (ssp != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(ssp)) {
+ filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
+ if (aIt != null) {
+ while (aIt.hasNext()) {
+ IntentFilter.AuthorityEntry a = aIt.next();
+ if (a.match(data) >= 0) {
+ int port = a.getPort();
+ filter.addDataAuthority(a.getHost(),
+ port >= 0 ? Integer.toString(port) : null);
+ break;
+ }
+ }
+ }
+ pIt = ri.filter.pathsIterator();
+ if (pIt != null) {
+ String path = data.getPath();
+ while (path != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(path)) {
+ filter.addDataPath(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (filter != null) {
+ final int N = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().size();
+ ComponentName[] set;
+ // If we don't add back in the component for forwarding the intent to a managed
+ // profile, the preferred activity may not be updated correctly (as the set of
+ // components we tell it we knew about will have changed).
+ final boolean needToAddBackProfileForwardingComponent =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;
+ if (!needToAddBackProfileForwardingComponent) {
+ set = new ComponentName[N];
+ } else {
+ set = new ComponentName[N + 1];
+ }
+
+ int bestMatch = 0;
+ for (int i=0; i<N; i++) {
+ ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().get(i).getResolveInfoAt(0);
+ set[i] = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.name);
+ if (r.match > bestMatch) bestMatch = r.match;
+ }
+
+ if (needToAddBackProfileForwardingComponent) {
+ set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolvedComponentName();
+ final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolveInfo().match;
+ if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
+ }
+
+ if (always) {
+ final int userId = getUserId();
+ final PackageManager pm = getPackageManager();
+
+ // Set the preferred Activity
+ pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
+
+ if (ri.handleAllWebDataURI) {
+ // Set default Browser if needed
+ final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
+ if (TextUtils.isEmpty(packageName)) {
+ pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+ }
+ }
+ } else {
+ try {
+ mMultiProfilePagerAdapter.getActiveListAdapter()
+ .mResolverListController.setLastChosen(intent, filter, bestMatch);
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
+ }
+ }
+ }
+ }
+
+ if (target != null) {
+ safelyStartActivity(target);
+
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ if (target.isSuspended()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void onActivityStarted(TargetInfo cti) {
+ // Do nothing
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return false;
+ }
+
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ return !target.isSuspended();
+ }
+
+ // TODO: this method takes an unused `UserHandle` because the override in `ChooserActivity` uses
+ // that data to set up other components as dependencies of the controller. In reality, these
+ // methods don't require polymorphism, because they're only invoked from within their respective
+ // concrete class; `ResolverActivity` will never call this method expecting to get a
+ // `ChooserListController` (subclass) result, because `ResolverActivity` only invokes this
+ // method as part of handling `createMultiProfilePagerAdapter()`, which is itself overridden in
+ // `ChooserActivity`. A future refactoring could better express the coupling between the adapter
+ // and controller types; in the meantime, structuring as an override (with matching signatures)
+ // shows that these methods are *structurally* related, and helps to prevent any regressions in
+ // the future if resolver *were* to make any (non-overridden) calls to a version that used a
+ // different signature (and thus didn't return the subclass type).
+ @VisibleForTesting
+ protected ResolverListController createListController(UserHandle userHandle) {
+ ResolverRankerServiceResolverComparator resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ null,
+ null,
+ getResolverRankerServiceUserHandleList(userHandle),
+ null);
+ return new ResolverListController(
+ this,
+ mPm,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ getAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ protected boolean postRebuildList(boolean rebuildCompleted) {
+ return postRebuildListInternal(rebuildCompleted);
+ }
+
+ void onHorizontalSwipeStateChanged(int state) {}
+
+ /**
+ * Callback called when user changes the profile tab.
+ * <p>This method is intended to be overridden by subclasses.
+ */
+ protected void onProfileTabSelected() { }
+
+ /**
+ * Add a label to signify that the user can pick a different app.
+ * @param adapter The adapter used to provide data to item views.
+ */
+ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
+ final boolean useHeader = adapter.hasFilteredItem();
+ if (useHeader) {
+ FrameLayout stub = findViewById(com.android.internal.R.id.stub);
+ stub.setVisibility(View.VISIBLE);
+ TextView textView = (TextView) LayoutInflater.from(this).inflate(
+ R.layout.resolver_different_item_header, null, false);
+ if (shouldShowTabs()) {
+ textView.setGravity(Gravity.CENTER);
+ }
+ stub.addView(textView);
+ }
+ }
+
+ protected void resetButtonBar() {
+ if (!mSupportsAlwaysUseOption) {
+ return;
+ }
+ final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonLayout == null) {
+ Log.e(TAG, "Layout unexpectedly does not have a button bar");
+ return;
+ }
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
+ if (!useLayoutWithDefault()) {
+ int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+ buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+ R.dimen.resolver_button_bar_spacing) + inset);
+ }
+ if (activeListAdapter.isTabLoaded()
+ && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
+ && !useLayoutWithDefault()) {
+ buttonLayout.setVisibility(View.INVISIBLE);
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.INVISIBLE);
+ }
+ setButtonBarIgnoreOffset(/* ignoreOffset */ false);
+ return;
+ }
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.VISIBLE);
+ }
+ buttonLayout.setVisibility(View.VISIBLE);
+ setButtonBarIgnoreOffset(/* ignoreOffset */ true);
+
+ mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
+ mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
+
+ resetAlwaysOrOnceButtonBar();
+ }
+
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_RESOLVER;
+ }
+
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
+ if (listAdapter.getUserHandle().equals(getAnnotatedUserHandles().workProfileUserHandle)
+ && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
+ // We have just turned on the work profile and entered the pass code to start it,
+ // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
+ // point in reloading the list now, since the work profile user is still
+ // turning on.
+ return;
+ }
+ boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ if (listRebuilt) {
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ activeListAdapter.notifyDataSetChanged();
+ if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) {
+ // We no longer have any items... just finish the activity.
+ finish();
+ }
+ }
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ }
+
+ protected void maybeLogProfileChange() {}
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected MyUserIdProvider createMyUserIdProvider() {
+ return new MyUserIdProvider();
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
+ return new WorkProfileAvailabilityManager(
+ getSystemService(UserManager.class),
+ getAnnotatedUserHandles().workProfileUserHandle,
+ this::onWorkProfileStatusUpdated);
+ }
+
+ protected void onWorkProfileStatusUpdated() {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ getAnnotatedUserHandles().workProfileUserHandle)) {
+ mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected ResolverListAdapter createResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ getTargetIntent(),
+ this,
+ initialIntentsUserSpace,
+ targetDataLoader);
+ }
+
+ private TargetDataLoader createIconLoader() {
+ Intent startIntent = getIntent();
+ boolean isAudioCaptureDevice =
+ startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ return new DefaultTargetDataLoader(this, getLifecycle(), isAudioCaptureDevice);
+ }
+
+ private LatencyTracker getLatencyTracker() {
+ return LatencyTracker.getInstance(this);
+ }
+
+ /**
+ * Get the string resource to be used as a label for the link to the resolver activity for an
+ * action.
+ *
+ * @param action The action to resolve
+ *
+ * @return The string resource to be used as a label
+ */
+ public static @StringRes int getLabelRes(String action) {
+ return ActionTitle.forAction(action).labelRes;
+ }
+
+ protected final EmptyStateProvider createEmptyStateProvider(
+ @Nullable UserHandle workProfileUserHandle) {
+ final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+ final EmptyStateProvider workProfileOffEmptyStateProvider =
+ new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
+ mWorkProfileAvailability,
+ /* onSwitchOnWorkSelectedListener= */
+ () -> {
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ },
+ getMetricsCategory());
+
+ final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ this,
+ workProfileUserHandle,
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ getMetricsCategory(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ );
+
+ // Return composite provider, the order matters (the higher, the more priority)
+ return new CompositeEmptyStateProvider(
+ blockerEmptyStateProvider,
+ workProfileOffEmptyStateProvider,
+ noAppsEmptyStateProvider
+ );
+ }
+
+ private Intent makeMyIntent() {
+ Intent intent = new Intent(getIntent());
+ intent.setComponent(null);
+ // The resolver activity is set to be hidden from recent tasks.
+ // we don't want this attribute to be propagated to the next activity
+ // being launched. Note that if the original Intent also had this
+ // flag set, we are now losing it. That should be a very rare case
+ // and we can live with this.
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
+ return intent;
+ }
+
+ /**
+ * Call {@link Activity#onCreate} without initializing anything further. This should
+ * only be used when the activity is about to be immediately finished to avoid wasting
+ * initializing steps and leaking resources.
+ */
+ protected final void super_onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ private ResolverMultiProfilePagerAdapter
+ createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ResolverListAdapter adapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ getAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ private UserHandle getIntentUser() {
+ return getIntent().hasExtra(EXTRA_CALLING_USER)
+ ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+ : getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ }
+
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ // In the edge case when we have 0 apps in the current profile and >1 apps in the other,
+ // the intent resolver is started in the other profile. Since this is the only case when
+ // this happens, we check for it here and set the current profile's tab.
+ int selectedProfile = getCurrentProfile();
+ UserHandle intentUser = getIntentUser();
+ if (!getAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
+ if (getAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_PERSONAL;
+ } else if (getAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_WORK;
+ }
+ } else {
+ int selectedProfileExtra = getSelectedProfileExtra();
+ if (selectedProfileExtra != -1) {
+ selectedProfile = selectedProfileExtra;
+ }
+ }
+ // We only show the default app for the profile of the current user. The filterLastUsed
+ // flag determines whether to show a default app and that app is not shown in the
+ // resolver list. So filterLastUsed should be false for the other profile.
+ ResolverListAdapter personalAdapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == getAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ UserHandle workProfileUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
+ ResolverListAdapter workAdapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == workProfileUserHandle.getIdentifier()),
+ /* userHandle */ workProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(workProfileUserHandle),
+ () -> mWorkProfileAvailability.isQuietModeEnabled(),
+ selectedProfile,
+ workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ /**
+ * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link
+ * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied.
+ * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE}
+ * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}
+ */
+ final int getSelectedProfileExtra() {
+ int selectedProfile = -1;
+ if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
+ selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
+ if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
+ throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
+ + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or "
+ + "ResolverActivity.PROFILE_WORK.");
+ }
+ }
+ return selectedProfile;
+ }
+
+ protected final @Profile int getCurrentProfile() {
+ UserHandle launchUser = getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ UserHandle personalUser = getAnnotatedUserHandles().personalProfileUserHandle;
+ return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
+ }
+
+ protected final AnnotatedUserHandles getAnnotatedUserHandles() {
+ return mLazyAnnotatedUserHandles.get();
+ }
+
+ private boolean hasWorkProfile() {
+ return getAnnotatedUserHandles().workProfileUserHandle != null;
+ }
+
+ private boolean hasCloneProfile() {
+ return getAnnotatedUserHandles().cloneProfileUserHandle != null;
+ }
+
+ protected final boolean isLaunchedAsCloneProfile() {
+ UserHandle launchUser = getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
+ UserHandle cloneUser = getAnnotatedUserHandles().cloneProfileUserHandle;
+ return hasCloneProfile() && launchUser.equals(cloneUser);
+ }
+
+ protected final boolean shouldShowTabs() {
+ return hasWorkProfile();
+ }
+
+ protected final void onProfileClick(View v) {
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri == null) {
+ return;
+ }
+
+ // Do not show the profile switch message anymore.
+ mProfileSwitchMessage = null;
+
+ onTargetSelected(dri, false);
+ finish();
+ }
+
+ private void updateIntentPickerPaddings() {
+ View titleCont = findViewById(com.android.internal.R.id.title_container);
+ titleCont.setPadding(
+ titleCont.getPaddingLeft(),
+ titleCont.getPaddingTop(),
+ titleCont.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_title_padding_bottom));
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ buttonBar.setPadding(
+ buttonBar.getPaddingLeft(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing),
+ buttonBar.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing));
+ }
+
+ private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
+ if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
+ return;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
+ .setBoolean(
+ currentUserHandle.equals(
+ getAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory(),
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
+ .write();
+ }
+
+ @Override // ResolverListCommunicator
+ public final void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();
+ final Option[] options = new Option[count];
+ for (int i = 0; i < options.length; i++) {
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);
+ if (target == null) {
+ // If this occurs, a new set of targets is being loaded. Let that complete,
+ // and have the next call to send voice choices proceed instead.
+ return;
+ }
+ options[i] = optionForChooserTarget(target, i);
+ }
+
+ mPickOptionRequest = new PickTargetOptionRequest(
+ new Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ final Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(getOrLoadDisplayLabel(target), index);
+ }
+
+ public final Intent getTargetIntent() {
+ return mIntents.isEmpty() ? null : mIntents.get(0);
+ }
+
+ protected final String getReferrerPackageName() {
+ final Uri referrer = getReferrer();
+ if (referrer != null && "android-app".equals(referrer.getScheme())) {
+ return referrer.getHost();
+ }
+ return null;
+ }
+
+ @Override // ResolverListCommunicator
+ public final void updateProfileViewButton() {
+ if (mProfileView == null) {
+ return;
+ }
+
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri != null && !shouldShowTabs()) {
+ mProfileView.setVisibility(View.VISIBLE);
+ View text = mProfileView.findViewById(com.android.internal.R.id.profile_button);
+ if (!(text instanceof TextView)) {
+ text = mProfileView.findViewById(com.android.internal.R.id.text1);
+ }
+ ((TextView) text).setText(dri.getDisplayLabel());
+ } else {
+ mProfileView.setVisibility(View.GONE);
+ }
+ }
+
+ private void setProfileSwitchMessage(int contentUserHint) {
+ if ((contentUserHint != UserHandle.USER_CURRENT)
+ && (contentUserHint != UserHandle.myUserId())) {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ UserInfo originUserInfo = userManager.getUserInfo(contentUserHint);
+ boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile()
+ : false;
+ boolean targetIsManaged = userManager.isManagedProfile();
+ if (originIsManaged && !targetIsManaged) {
+ mProfileSwitchMessage = getForwardToPersonalMsg();
+ } else if (!originIsManaged && targetIsManaged) {
+ mProfileSwitchMessage = getForwardToWorkMsg();
+ }
+ }
+ }
+
+ private String getForwardToPersonalMsg() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_PERSONAL,
+ () -> getString(R.string.forward_intent_to_owner));
+ }
+
+ private String getForwardToWorkMsg() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_WORK,
+ () -> getString(R.string.forward_intent_to_work));
+ }
+
+ protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ final ActionTitle title = mResolvingHome
+ ? ActionTitle.HOME
+ : ActionTitle.forAction(intent.getAction());
+
+ // While there may already be a filtered item, we can only use it in the title if the list
+ // is already sorted and all information relevant to it is already in the list.
+ final boolean named =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;
+ if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+ return getString(defaultTitleRes);
+ } else {
+ return named
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
+ : getString(title.titleRes);
+ }
+ }
+
+ final void dismiss() {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ @Override
+ protected final void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ false);
+ if (shouldShowTabs()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().workProfileUserHandle,
+ false);
+ }
+ mRegistered = true;
+ }
+ if (shouldShowTabs() && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
+ if (mWorkProfileAvailability.isQuietModeEnabled()) {
+ mWorkProfileAvailability.markWorkProfileEnabledBroadcastReceived();
+ }
+ }
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected final void onStart() {
+ super.onStart();
+
+ this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ if (shouldShowTabs()) {
+ mWorkProfileAvailability.registerWorkProfileStateReceiver(this);
+ }
+ }
+
+ @Override
+ protected final void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
+ }
+ }
+
+ @Override
+ protected final void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ resetButtonBar();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+
+ private boolean hasManagedProfile() {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (userManager == null) {
+ return false;
+ }
+
+ try {
+ List<UserInfo> profiles = userManager.getProfiles(getUserId());
+ for (UserInfo userInfo : profiles) {
+ if (userInfo != null && userInfo.isManagedProfile()) {
+ return true;
+ }
+ }
+ } catch (SecurityException e) {
+ return false;
+ }
+ return false;
+ }
+
+ private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
+ try {
+ ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+ resolveInfo.activityInfo.packageName, 0 /* default flags */);
+ return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
+ boolean filtered) {
+ if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {
+ // Never allow the inactive profile to always open an app.
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ // In case of clonedProfile being active, we do not allow the 'Always' option in the
+ // disambiguation dialog of Personal Profile as the package manager cannot distinguish
+ // between cross-profile preferred activities.
+ if (hasCloneProfile() && (mMultiProfilePagerAdapter.getCurrentPage() == PROFILE_PERSONAL)) {
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ boolean enabled = false;
+ ResolveInfo ri = null;
+ if (hasValidSelection) {
+ ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(checkedPos, filtered);
+ if (ri == null) {
+ Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled");
+ return;
+ } else if (ri.targetUserId != UserHandle.USER_CURRENT) {
+ Log.e(TAG, "Attempted to set selection to resolve info for another user");
+ return;
+ } else {
+ enabled = true;
+ }
+
+ mAlwaysButton.setText(getResources()
+ .getString(R.string.activity_resolver_use_always));
+ }
+
+ if (ri != null) {
+ ActivityInfo activityInfo = ri.activityInfo;
+
+ boolean hasRecordPermission =
+ mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO,
+ activityInfo.packageName)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!hasRecordPermission) {
+ // OK, we know the record permission, is this a capture device
+ boolean hasAudioCapture =
+ getIntent().getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ enabled = !hasAudioCapture;
+ }
+ }
+ mAlwaysButton.setEnabled(enabled);
+ }
+
+ private String getWorkProfileNotSupportedMsg(String launcherName) {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_PROFILE_NOT_SUPPORTED,
+ () -> getString(
+ R.string.activity_resolver_work_profiles_support,
+ launcherName),
+ launcherName);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
+ boolean rebuildCompleted) {
+ if (isAutolaunching()) {
+ return;
+ }
+ if (mIsIntentPicker) {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .setUseLayoutWithDefault(useLayoutWithDefault());
+ }
+ if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {
+ mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);
+ } else {
+ mMultiProfilePagerAdapter.showListView(listAdapter);
+ }
+ // showEmptyResolverListEmptyState can mark the tab as loaded,
+ // which is a precondition for auto launching
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return;
+ }
+ if (doPostProcessing) {
+ maybeCreateHeader(listAdapter);
+ resetButtonBar();
+ onListRebuilt(listAdapter, rebuildCompleted);
+ }
+ }
+
+ /** Start the activity specified by the {@link TargetInfo}.*/
+ public final void safelyStartActivity(TargetInfo cti) {
+ // In case cloned apps are present, we would want to start those apps in cloned user
+ // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle
+ // identifies the correct user space in such cases.
+ UserHandle activityUserHandle = cti.getResolveInfo().userHandle;
+ safelyStartActivityAsUser(cti, activityUserHandle, null);
+ }
+
+ /**
+ * Start activity as a fixed user handle.
+ * @param cti TargetInfo to be launched.
+ * @param user User to launch this activity as.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
+ safelyStartActivityAsUser(cti, user, null);
+ }
+
+ protected final void safelyStartActivityAsUser(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // We're dispatching intents that might be coming from legacy apps, so
+ // don't kill ourselves.
+ StrictMode.disableDeathOnFileUriExposure();
+ try {
+ safelyStartActivityInternal(cti, user, options);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ }
+
+ @VisibleForTesting
+ protected void safelyStartActivityInternal(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // If the target is suspended, the activity will not be successfully launched.
+ // Do not unregister from package manager updates in this case
+ if (!cti.isSuspended() && mRegistered) {
+ if (mPersonalPackageMonitor != null) {
+ mPersonalPackageMonitor.unregister();
+ }
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ // If needed, show that intent is forwarded
+ // from managed profile to owner or other way around.
+ if (mProfileSwitchMessage != null) {
+ Toast.makeText(this, mProfileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ if (!mSafeForwardingMode) {
+ if (cti.startAsUser(this, options, user)) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ return;
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG,
+ "Unable to launch as uid " + getAnnotatedUserHandles().userIdOfCallingApp
+ + " package " + getLaunchedFromPackage() + ", while running in "
+ + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ final void showTargetDetails(ResolveInfo ri) {
+ Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle());
+ }
+
+ /**
+ * Sets up the content view.
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ private boolean configureContentView(TargetDataLoader targetDataLoader) {
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) {
+ throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() "
+ + "cannot be null.");
+ }
+ Trace.beginSection("configureContentView");
+ // We partially rebuild the inactive adapter to determine if we should auto launch
+ // isTabLoaded will be true here if the empty state screen is shown instead of the list.
+ boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildActiveTab(true)
+ || mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded();
+ if (shouldShowTabs()) {
+ boolean rebuildInactiveCompleted = mMultiProfilePagerAdapter.rebuildInactiveTab(false)
+ || mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded();
+ rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
+ }
+
+ if (shouldUseMiniResolver()) {
+ configureMiniResolverContent(targetDataLoader);
+ Trace.endSection();
+ return false;
+ }
+
+ if (useLayoutWithDefault()) {
+ mLayoutId = R.layout.resolver_list_with_default;
+ } else {
+ mLayoutId = getLayoutResource();
+ }
+ setContentView(mLayoutId);
+ mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager));
+ boolean result = postRebuildList(rebuildCompleted);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Mini resolver is shown when the user is choosing between browser[s] in this profile and a
+ * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
+ * and asks the user if they'd like to open that cross-profile app or use the in-profile
+ * browser.
+ */
+ private void configureMiniResolverContent(TargetDataLoader targetDataLoader) {
+ mLayoutId = R.layout.miniresolver;
+ setContentView(mLayoutId);
+
+ DisplayResolveInfo sameProfileResolveInfo =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFirstDisplayResolveInfo();
+ boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
+
+ final ResolverListAdapter inactiveAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ final DisplayResolveInfo otherProfileResolveInfo =
+ inactiveAdapter.getFirstDisplayResolveInfo();
+
+ // Load the icon asynchronously
+ ImageView icon = findViewById(com.android.internal.R.id.icon);
+ targetDataLoader.loadAppTargetIcon(
+ otherProfileResolveInfo,
+ inactiveAdapter.getUserHandle(),
+ (drawable) -> {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ });
+
+ ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
+ getResources().getString(
+ inWorkProfile
+ ? R.string.miniresolver_open_in_personal
+ : R.string.miniresolver_open_in_work,
+ getOrLoadDisplayLabel(otherProfileResolveInfo)));
+ ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
+ inWorkProfile ? R.string.miniresolver_use_work_browser
+ : R.string.miniresolver_use_personal_browser);
+
+ findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener(
+ v -> {
+ safelyStartActivity(sameProfileResolveInfo);
+ finish();
+ });
+
+ findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
+ Intent intent = otherProfileResolveInfo.getResolvedIntent();
+ safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle());
+ finish();
+ });
+ }
+
+ /**
+ * Mini resolver should be used when all of the following are true:
+ * 1. This is the intent picker (ResolverActivity).
+ * 2. This profile only has web browser matches.
+ * 3. The other profile has a single non-browser match.
+ */
+ private boolean shouldUseMiniResolver() {
+ if (!mIsIntentPicker) {
+ return false;
+ }
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null
+ || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+ return false;
+ }
+ ResolverListAdapter sameProfileAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ ResolverListAdapter otherProfileAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+
+ if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
+ Log.d(TAG, "No targets in the current profile");
+ return false;
+ }
+
+ if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
+ Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
+ return false;
+ }
+
+ if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Other profile is a web browser");
+ return false;
+ }
+
+ if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Non-browser found in this profile");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ final boolean postRebuildListInternal(boolean rebuildCompleted) {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+
+ // We only rebuild asynchronously when we have multiple elements to sort. In the case where
+ // we're already done, we can check if we should auto-launch immediately.
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return true;
+ }
+
+ setupViewVisibilities();
+
+ if (shouldShowTabs()) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
+ private int isPermissionGranted(String permission, int uid) {
+ return ActivityManager.checkComponentPermission(permission, uid,
+ /* owningUid= */-1, /* exported= */ true);
+ }
+
+ /**
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
+ */
+ private boolean maybeAutolaunchActivity() {
+ int numberOfProfiles = mMultiProfilePagerAdapter.getItemCount();
+ if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) {
+ return true;
+ } else if (numberOfProfiles == 2
+ && mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded()
+ && mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded()
+ && maybeAutolaunchIfCrossProfileSupported()) {
+ // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the
+ // correct intent-picker UIs (e.g., mini-resolver) if it was launched without
+ // ACTION_SEND.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean maybeAutolaunchIfSingleTarget() {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
+ return false;
+ }
+
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * When we have a personal and a work profile, we auto launch in the following scenario:
+ * - There is 1 resolved target on each profile
+ * - That target is the same app on both profiles
+ * - The target app has permission to communicate cross profiles
+ * - The target app has declared it supports cross-profile communication via manifest metadata
+ */
+ private boolean maybeAutolaunchIfCrossProfileSupported() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int count = activeListAdapter.getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+ ResolverListAdapter inactiveListAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ if (inactiveListAdapter.getUnfilteredCount() != 1) {
+ return false;
+ }
+ TargetInfo activeProfileTarget = activeListAdapter
+ .targetInfoForPosition(0, false);
+ TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
+ if (!Objects.equals(activeProfileTarget.getResolvedComponentName(),
+ inactiveProfileTarget.getResolvedComponentName())) {
+ return false;
+ }
+ if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
+ return false;
+ }
+ String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
+ if (!canAppInteractCrossProfiles(packageName)) {
+ return false;
+ }
+
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(getAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
+ }
+
+ /**
+ * Returns whether the package has the necessary permissions to interact across profiles on
+ * behalf of a given user.
+ *
+ * <p>This means meeting the following condition:
+ * <ul>
+ * <li>The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least
+ * one of the following conditions must be fulfilled</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding
+ * AppOps {@code android:interact_across_profiles} is set to "allow".</li>
+ * </ul>
+ *
+ */
+ private boolean canAppInteractCrossProfiles(String packageName) {
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Package " + packageName + " does not exist on current user.");
+ return false;
+ }
+ if (!applicationInfo.crossProfile) {
+ return false;
+ }
+
+ int packageUid = applicationInfo.uid;
+
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ packageUid) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS, packageUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES,
+ PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
+ }
+
+ private void setupProfileTabs() {
+ maybeHideDivider();
+ TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ tabHost.setup();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSaveEnabled(false);
+
+ Button personalButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ personalButton.setText(getPersonalTabLabel());
+ personalButton.setContentDescription(getPersonalTabAccessibilityLabel());
+
+ TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(personalButton);
+ tabHost.addTab(tabSpec);
+
+ Button workButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ workButton.setText(getWorkTabLabel());
+ workButton.setContentDescription(getWorkTabAccessibilityLabel());
+
+ tabSpec = tabHost.newTabSpec(TAB_TAG_WORK)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(workButton);
+ tabHost.addTab(tabSpec);
+
+ TabWidget tabWidget = tabHost.getTabWidget();
+ tabWidget.setVisibility(View.VISIBLE);
+ updateActiveTabStyle(tabHost);
+
+ tabHost.setOnTabChangedListener(tabId -> {
+ updateActiveTabStyle(tabHost);
+ if (TAB_TAG_PERSONAL.equals(tabId)) {
+ viewPager.setCurrentItem(0);
+ } else {
+ viewPager.setCurrentItem(1);
+ }
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ onProfileTabSelected();
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(viewPager.getCurrentItem())
+ .setStrings(getMetricsCategory())
+ .write();
+ });
+
+ viewPager.setVisibility(View.VISIBLE);
+ tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
+ mMultiProfilePagerAdapter.setOnProfileSelectedListener(
+ new MultiProfilePagerAdapter.OnProfileSelectedListener() {
+ @Override
+ public void onProfileSelected(int index) {
+ tabHost.setCurrentTab(index);
+ resetButtonBar();
+ resetCheckedItem();
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ onHorizontalSwipeStateChanged(state);
+ }
+ });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ final View workTab = tabHost.getTabWidget().getChildAt(1);
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
+ }
+
+ private String getPersonalTabLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_PERSONAL_TAB, () -> getString(R.string.resolver_personal_tab));
+ }
+
+ private String getWorkTabLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_TAB, () -> getString(R.string.resolver_work_tab));
+ }
+
+ private void maybeHideDivider() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ final View divider = findViewById(com.android.internal.R.id.divider);
+ if (divider == null) {
+ return;
+ }
+ divider.setVisibility(View.GONE);
+ }
+
+ private void resetCheckedItem() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ mLastSelected = ListView.INVALID_POSITION;
+ ListView inactiveListView = (ListView) mMultiProfilePagerAdapter.getInactiveAdapterView();
+ if (inactiveListView.getCheckedItemCount() > 0) {
+ inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
+ }
+ }
+
+ private String getPersonalTabAccessibilityLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_PERSONAL_TAB_ACCESSIBILITY,
+ () -> getString(R.string.resolver_personal_tab_accessibility));
+ }
+
+ private String getWorkTabAccessibilityLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_TAB_ACCESSIBILITY,
+ () -> getString(R.string.resolver_work_tab_accessibility));
+ }
+
+ private static int getAttrColor(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ return colorAccent;
+ }
+
+ private void updateActiveTabStyle(TabHost tabHost) {
+ int currentTab = tabHost.getCurrentTab();
+ TextView selected = (TextView) tabHost.getTabWidget().getChildAt(currentTab);
+ TextView unselected = (TextView) tabHost.getTabWidget().getChildAt(1 - currentTab);
+ selected.setSelected(true);
+ unselected.setSelected(false);
+ }
+
+ private void setupViewVisibilities() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+ addUseDifferentAppLabelIfNecessary(activeListAdapter);
+ }
+ }
+
+ /**
+ * Updates the button bar container {@code ignoreOffset} layout param.
+ * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of
+ * the screen.
+ */
+ private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
+ View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ if (buttonBarContainer != null) {
+ ResolverDrawerLayout.LayoutParams layoutParams =
+ (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
+ layoutParams.ignoreOffset = ignoreOffset;
+ buttonBarContainer.setLayoutParams(layoutParams);
+ }
+ }
+
+ private void setupAdapterListView(ListView listView, ItemClickListener listener) {
+ listView.setOnItemClickListener(listener);
+ listView.setOnItemLongClickListener(listener);
+
+ if (mSupportsAlwaysUseOption) {
+ listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ }
+ }
+
+ /**
+ * Configure the area above the app selection list (title, content preview, etc).
+ */
+ private void maybeCreateHeader(ResolverListAdapter listAdapter) {
+ if (mHeaderCreatorUser != null
+ && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
+ return;
+ }
+ if (!shouldShowTabs()
+ && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setVisibility(View.GONE);
+ }
+ }
+
+ CharSequence title = mTitle != null
+ ? mTitle
+ : getTitleForAction(getTargetIntent(), mDefaultTitleResId);
+
+ if (!TextUtils.isEmpty(title)) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ setTitle(title);
+ }
+
+ final ImageView iconView = findViewById(com.android.internal.R.id.icon);
+ if (iconView != null) {
+ listAdapter.loadFilteredItemIconTaskAsync(iconView);
+ }
+ mHeaderCreatorUser = listAdapter.getUserHandle();
+ }
+
+ private void resetAlwaysOrOnceButtonBar() {
+ // Disable both buttons initially
+ setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false);
+ mOnceButton.setEnabled(false);
+
+ int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getFilteredPosition();
+ if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, filteredPosition, false);
+ mOnceButton.setEnabled(true);
+ // Focus the button if we already have the default option
+ mOnceButton.requestFocus();
+ return;
+ }
+
+ // When the items load in, if an item was already selected, enable the buttons
+ ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ if (currentAdapterView != null
+ && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true);
+ mOnceButton.setEnabled(true);
+ }
+ }
+
+ @Override // ResolverListCommunicator
+ public final boolean useLayoutWithDefault() {
+ // We only use the default app layout when the profile of the active user has a
+ // filtered item. We always show the same default app even in the inactive user profile.
+ boolean adapterForCurrentUserHasFilteredItem =
+ mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch).hasFilteredItem();
+ return mSupportsAlwaysUseOption && adapterForCurrentUserHasFilteredItem;
+ }
+
+ /**
+ * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets
+ * called and we are launched in a new task.
+ */
+ protected final void setRetainInOnStop(boolean retainInOnStop) {
+ mRetainInOnStop = retainInOnStop;
+ }
+
+ private boolean inactiveListAdapterHasItems() {
+ if (!shouldShowTabs()) {
+ return false;
+ }
+ return mMultiProfilePagerAdapter.getInactiveListAdapter().getCount() > 0;
+ }
+
+ final class ItemClickListener implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return;
+ }
+ // If we're still loading, we can't yet enable the buttons.
+ if (mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true) == null) {
+ return;
+ }
+ ListView currentAdapterView =
+ (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ final int checkedPos = currentAdapterView.getCheckedItemPosition();
+ final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
+ if (!useLayoutWithDefault()
+ && (!hasValidSelection || mLastSelected != checkedPos)
+ && mAlwaysButton != null) {
+ setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
+ mOnceButton.setEnabled(hasValidSelection);
+ if (hasValidSelection) {
+ currentAdapterView.smoothScrollToPosition(checkedPos);
+ mOnceButton.requestFocus();
+ }
+ mLastSelected = checkedPos;
+ } else {
+ startSelected(position, false, true);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return false;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true);
+ showTargetDetails(ri);
+ return true;
+ }
+
+ }
+
+ /** Determine whether a given match result is considered "specific" in our application. */
+ public static final boolean isSpecificUriMatch(int match) {
+ match = (match & IntentFilter.MATCH_CATEGORY_MASK);
+ return match >= IntentFilter.MATCH_CATEGORY_HOST
+ && match <= IntentFilter.MATCH_CATEGORY_PATH;
+ }
+
+ static final class PickTargetOptionRequest extends PickOptionRequest {
+ public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ super(prompt, options, extras);
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ super.onPickOptionResult(finished, selections, result);
+ if (selections.length != 1) {
+ // TODO In a better world we would filter the UI presented here and let the
+ // user refine. Maybe later.
+ return;
+ }
+
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getItem(selections[0].getIndex());
+ if (ra.onTargetSelected(ti, false)) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+ }
+ }
+ /**
+ * Returns the {@link UserHandle} to use when querying resolutions for intents in a
+ * {@link ResolverListController} configured for the provided {@code userHandle}.
+ */
+ protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
+ return getAnnotatedUserHandles().getQueryIntentsUser(userHandle);
+ }
+
+ /**
+ * Returns the {@link List} of {@link UserHandle} to pass on to the
+ * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}.
+ */
+ @VisibleForTesting(visibility = PROTECTED)
+ public final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+ return getResolverRankerServiceUserHandleListInternal(userHandle);
+ }
+
+ @VisibleForTesting
+ protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(
+ UserHandle userHandle) {
+ List<UserHandle> userList = new ArrayList<>();
+ userList.add(userHandle);
+ // Add clonedProfileUserHandle to the list only if we are:
+ // a. Building the Personal Tab.
+ // b. CloneProfile exists on the device.
+ if (userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ && hasCloneProfile()) {
+ userList.add(getAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+ return userList;
+ }
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
+}
diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index 35dc2ee6..03e32c65 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -27,6 +27,8 @@
<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"
diff --git a/java/tests/src/com/android/intentresolver/MatcherUtils.java b/java/tests/src/com/android/intentresolver/MatcherUtils.java
index 6168968b..97cc6984 100644
--- a/java/tests/src/com/android/intentresolver/MatcherUtils.java
+++ b/java/tests/src/com/android/intentresolver/MatcherUtils.java
@@ -29,7 +29,7 @@ public class MatcherUtils {
/**
* Returns a {@link Matcher} which only matches the first occurrence of a set criteria.
*/
- static <T> Matcher<T> first(final Matcher<T> matcher) {
+ public static <T> Matcher<T> first(final Matcher<T> matcher) {
return new BaseMatcher<T>() {
boolean isFirstMatch = true;
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
index 1f8d9bee..4eb350fc 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -43,7 +43,7 @@ public class ResolverDataProvider {
createResolveInfo(i, UserHandle.USER_CURRENT));
}
- static ResolvedComponentInfo createResolvedComponentInfo(int i,
+ public static ResolvedComponentInfo createResolvedComponentInfo(int i,
UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
@@ -59,7 +59,7 @@ public class ResolverDataProvider {
createResolveInfo(componentName, UserHandle.USER_CURRENT));
}
- static ResolvedComponentInfo createResolvedComponentInfo(
+ public static ResolvedComponentInfo createResolvedComponentInfo(
ComponentName componentName, Intent intent, UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
componentName,
@@ -74,8 +74,8 @@ public class ResolverDataProvider {
createResolveInfo(i, USER_SOMEONE_ELSE));
}
- static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
- UserHandle resolvedForUser) {
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
createResolverIntent(i),
@@ -89,7 +89,7 @@ public class ResolverDataProvider {
createResolveInfo(i, userId));
}
- static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
int userId, UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
diff --git a/java/tests/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java
new file mode 100644
index 00000000..32eabbed
--- /dev/null
+++ b/java/tests/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/java/tests/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
new file mode 100644
index 00000000..41b31d01
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
@@ -0,0 +1,280 @@
+/*
+ * 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.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.lifecycle.ViewModelProvider;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.ChooserIntegratedDeviceComponents;
+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.WorkProfileAvailabilityManager;
+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();
+ }
+
+ @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/java/tests/src/com/android/intentresolver/v2/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/v2/ResolverActivityTest.java
new file mode 100644
index 00000000..f0911833
--- /dev/null
+++ b/java/tests/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/java/tests/src/com/android/intentresolver/v2/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/v2/ResolverWrapperActivity.java
new file mode 100644
index 00000000..610d031e
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/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.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.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(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.
+ */
+ 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<CharSequence[]> 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/java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java
new file mode 100644
index 00000000..1e74c7a5
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java
@@ -0,0 +1,3160 @@
+/*
+ * 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.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.MatcherUtils.first;
+import static com.android.intentresolver.v2.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
+import static com.android.intentresolver.v2.ChooserActivity.TARGET_TYPE_DEFAULT;
+import static com.android.intentresolver.v2.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+import static com.android.intentresolver.v2.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+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.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+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.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import dagger.hilt.android.testing.HiltAndroidRule;
+import dagger.hilt.android.testing.HiltAndroidTest;
+
+/**
+ * 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
+ public void testSlowUriMetadata_fallbackToFilePreview() throws InterruptedException {
+ Uri uri = createTestContentProviderUri(
+ "application/pdf", "image/png", /*streamTypeTimeout=*/4_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);
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
+ .isTrue();
+ 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 testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi()
+ throws InterruptedException {
+ Uri fileUri = createTestContentProviderUri(
+ "application/pdf", "application/pdf", /*streamTypeTimeout=*/150);
+ 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);
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
+ .isTrue();
+
+ 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
+ public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart()
+ throws InterruptedException {
+ 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", 3_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);
+
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 1_000))
+ .isTrue();
+ 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(R.id.scrollable_image_preview))
+ .check(matches(isDisplayed()));
+
+ onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp());
+ waitForIdle();
+
+ onView(withId(R.id.chooser_headline_row_container))
+ .check(matches(isCompletelyDisplayed()));
+ onView(withId(R.id.headline))
+ .check(matches(isDisplayed()));
+ onView(withId(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 boolean launchActivityWithTimeout(Intent intent, long timeout)
+ throws InterruptedException {
+ final int initialState = 0;
+ final int completedState = 1;
+ final int timeoutState = 2;
+ final AtomicInteger state = new AtomicInteger(initialState);
+ final CountDownLatch cdl = new CountDownLatch(1);
+
+ ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
+ try {
+ executor.execute(() -> {
+ mActivityRule.launchActivity(intent);
+ state.compareAndSet(initialState, completedState);
+ cdl.countDown();
+ });
+ executor.schedule(
+ () -> {
+ state.compareAndSet(initialState, timeoutState);
+ cdl.countDown();
+ },
+ timeout,
+ TimeUnit.MILLISECONDS);
+ cdl.await();
+ return state.get() == completedState;
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ 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/java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java
new file mode 100644
index 00000000..e4ec1776
--- /dev/null
+++ b/java/tests/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();
+ }
+ }
+}