summaryrefslogtreecommitdiff
path: root/java/src/com
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 /java/src/com
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
Diffstat (limited to 'java/src/com')
-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
14 files changed, 4350 insertions, 37 deletions
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;
+ }
+}