Merge "Do the sorting for the ShareSheet asynchronously."
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index c314cae..cb7be2e 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -69,6 +69,7 @@
 import android.widget.BaseAdapter;
 import android.widget.ListView;
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.ResolverActivity.TargetInfo;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -93,6 +94,7 @@
     private IntentSender mRefinementIntentSender;
     private RefinementResultReceiver mRefinementResultReceiver;
     private ChooserTarget[] mCallerChooserTargets;
+    private ComponentName[] mFilteredComponentNames;
 
     private Intent mReferrerFillInIntent;
 
@@ -235,7 +237,7 @@
                 }
                 names[i] = (ComponentName) pa[i];
             }
-            setFilteredComponents(names);
+            mFilteredComponentNames = names;
         }
 
         pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
@@ -642,17 +644,65 @@
         }
     }
 
+    public class ChooserListController extends ResolverListController {
+        public ChooserListController(Context context,
+                PackageManager pm,
+                Intent targetIntent,
+                String referrerPackageName,
+                int launchedFromUid) {
+            super(context, pm, targetIntent, referrerPackageName, launchedFromUid);
+        }
+
+        @Override
+        boolean isComponentPinned(ComponentName name) {
+            return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+        }
+
+        @Override
+        boolean isComponentFiltered(ComponentName name) {
+            if (mFilteredComponentNames == null) {
+                return false;
+            }
+            for (ComponentName filteredComponentName : mFilteredComponentNames) {
+                if (name.equals(filteredComponentName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public float getScore(DisplayResolveInfo target) {
+            if (target == null) {
+                return CALLER_TARGET_SCORE_BOOST;
+            }
+            float score = super.getScore(target);
+            if (target.isPinned()) {
+                score += PINNED_TARGET_SCORE_BOOST;
+            }
+            return score;
+        }
+    }
+
     @Override
     public ResolveListAdapter createAdapter(Context context, List<Intent> payloadIntents,
             Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
             boolean filterLastUsed) {
         final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents,
-                initialIntents, rList, launchedFromUid, filterLastUsed);
-        if (DEBUG) Log.d(TAG, "Adapter created; querying services");
-        queryTargetServices(adapter);
+                initialIntents, rList, launchedFromUid, filterLastUsed, createListController());
         return adapter;
     }
 
+    @VisibleForTesting
+    protected ResolverListController createListController() {
+        return new ChooserListController(
+                this,
+                mPm,
+                getTargetIntent(),
+                getReferrerPackageName(),
+                mLaunchedFromUid);
+    }
+
     final class ChooserTargetInfo implements TargetInfo {
         private final DisplayResolveInfo mSourceInfo;
         private final ResolveInfo mBackupResolveInfo;
@@ -853,10 +903,11 @@
 
         public ChooserListAdapter(Context context, List<Intent> payloadIntents,
                 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
-                boolean filterLastUsed) {
+                boolean filterLastUsed, ResolverListController resolverListController) {
             // Don't send the initial intents through the shared ResolverActivity path,
             // we want to separate them into a different section.
-            super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed);
+            super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed,
+                    resolverListController);
 
             if (initialIntents != null) {
                 final PackageManager pm = getPackageManager();
@@ -922,18 +973,6 @@
         }
 
         @Override
-        public float getScore(DisplayResolveInfo target) {
-            if (target == null) {
-                return CALLER_TARGET_SCORE_BOOST;
-            }
-            float score = super.getScore(target);
-            if (target.isPinned()) {
-                score += PINNED_TARGET_SCORE_BOOST;
-            }
-            return score;
-        }
-
-        @Override
         public View onCreateView(ViewGroup parent) {
             return mInflater.inflate(
                     com.android.internal.R.layout.resolve_grid_item, parent, false);
@@ -944,6 +983,8 @@
             if (mServiceTargets != null) {
                 pruneServiceTargets();
             }
+            if (DEBUG) Log.d(TAG, "List built querying services");
+            queryTargetServices(this);
         }
 
         @Override
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index c516b5c..7c22c4f 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -16,15 +16,14 @@
 
 package com.android.internal.app;
 
-import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringRes;
+import android.annotation.UiThread;
 import android.app.Activity;
 import android.app.ActivityThread;
 import android.app.VoiceInteractor.PickOptionRequest;
 import android.app.VoiceInteractor.PickOptionRequest.Option;
 import android.app.VoiceInteractor.Prompt;
-import android.content.pm.ComponentInfo;
 import android.os.AsyncTask;
 import android.os.RemoteException;
 import android.provider.MediaStore;
@@ -33,6 +32,7 @@
 import android.util.Slog;
 import android.widget.AbsListView;
 import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 
 import android.app.ActivityManager;
@@ -75,7 +75,6 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
@@ -83,21 +82,16 @@
 import java.util.Set;
 
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
 
 /**
  * This activity is displayed when the system attempts to start an Intent for
  * which there is more than one matching activity, allowing the user to decide
  * which to go to.  It is not normally used directly by application developers.
  */
+@UiThread
 public class ResolverActivity extends Activity {
-    private static final String TAG = "ResolverActivity";
-    private static final boolean DEBUG = false;
 
-    private int mLaunchedFromUid;
-    private ResolveListAdapter mAdapter;
-    private PackageManager mPm;
+    protected ResolveListAdapter mAdapter;
     private boolean mSafeForwardingMode;
     private boolean mAlwaysUseOption;
     private AbsListView mAdapterView;
@@ -108,13 +102,18 @@
     private int mLastSelected = AbsListView.INVALID_POSITION;
     private boolean mResolvingHome = false;
     private int mProfileSwitchMessageId = -1;
+    private int mLayoutId;
     private final ArrayList<Intent> mIntents = new ArrayList<>();
-    private ResolverComparator mResolverComparator;
     private PickTargetOptionRequest mPickOptionRequest;
-    private ComponentName[] mFilteredComponents;
+    private String mReferrerPackage;
 
     protected ResolverDrawerLayout mResolverDrawerLayout;
     protected String mContentType;
+    protected PackageManager mPm;
+    protected int mLaunchedFromUid;
+
+    private static final String TAG = "ResolverActivity";
+    private static final boolean DEBUG = false;
 
     private boolean mRegistered;
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
@@ -261,6 +260,7 @@
 
         mPackageMonitor.register(this, getMainLooper(), false);
         mRegistered = true;
+        mReferrerPackage = getReferrerPackageName();
 
         final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
         mIconDpi = am.getLauncherLargeIconDensity();
@@ -268,11 +268,6 @@
         // Add our initial intent as the first item, regardless of what else has already been added.
         mIntents.add(0, new Intent(intent));
 
-        final String referrerPackage = getReferrerPackageName();
-
-        mResolverComparator = new ResolverComparator(this, getTargetIntent(), referrerPackage);
-        mContentType = mResolverComparator.mContentType;
-
         if (configureContentView(mIntents, initialIntents, rList, alwaysUseOption)) {
             return;
         }
@@ -306,11 +301,11 @@
             if (titleIcon != null) {
                 ApplicationInfo ai = null;
                 try {
-                    if (!TextUtils.isEmpty(referrerPackage)) {
-                        ai = mPm.getApplicationInfo(referrerPackage, 0);
+                    if (!TextUtils.isEmpty(mReferrerPackage)) {
+                        ai = mPm.getApplicationInfo(mReferrerPackage, 0);
                     }
                 } catch (NameNotFoundException e) {
-                    Log.e(TAG, "Could not find referrer package " + referrerPackage);
+                    Log.e(TAG, "Could not find referrer package " + mReferrerPackage);
                 }
 
                 if (ai != null) {
@@ -372,24 +367,6 @@
                         + (categories != null ? Arrays.toString(categories.toArray()) : ""));
     }
 
-    public final void setFilteredComponents(ComponentName[] components) {
-        mFilteredComponents = components;
-    }
-
-    public final boolean isComponentFiltered(ComponentInfo component) {
-        if (mFilteredComponents == null) {
-            return false;
-        }
-
-        final ComponentName checkName = component.getComponentName();
-        for (ComponentName name : mFilteredComponents) {
-            if (name.equals(checkName)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     /**
      * Perform any initialization needed for voice interaction.
      */
@@ -431,7 +408,7 @@
         return mIntents.isEmpty() ? null : mIntents.get(0);
     }
 
-    private String getReferrerPackageName() {
+    protected String getReferrerPackageName() {
         final Uri referrer = getReferrer();
         if (referrer != null && "android-app".equals(referrer.getScheme())) {
             return referrer.getHost();
@@ -689,7 +666,7 @@
         final Intent intent = target != null ? target.getResolvedIntent() : null;
 
         if (intent != null && (mAlwaysUseOption || mAdapter.hasFilteredItem())
-                && mAdapter.mOrigResolveList != null) {
+                && mAdapter.mUnfilteredResolveList != null) {
             // Build a reasonable intent filter, based on what matched.
             IntentFilter filter = new IntentFilter();
             Intent filterIntent;
@@ -774,11 +751,11 @@
             }
 
             if (filter != null) {
-                final int N = mAdapter.mOrigResolveList.size();
+                final int N = mAdapter.mUnfilteredResolveList.size();
                 ComponentName[] set = new ComponentName[N];
                 int bestMatch = 0;
                 for (int i=0; i<N; i++) {
-                    ResolveInfo r = mAdapter.mOrigResolveList.get(i).getResolveInfoAt(0);
+                    ResolveInfo r = mAdapter.mUnfilteredResolveList.get(i).getResolveInfoAt(0);
                     set[i] = new ComponentName(r.activityInfo.packageName,
                             r.activityInfo.name);
                     if (r.match > bestMatch) bestMatch = r.match;
@@ -899,7 +876,17 @@
             Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
             boolean filterLastUsed) {
         return new ResolveListAdapter(context, payloadIntents, initialIntents, rList,
-                launchedFromUid, filterLastUsed);
+                launchedFromUid, filterLastUsed, createListController());
+    }
+
+    @VisibleForTesting
+    protected ResolverListController createListController() {
+        return new ResolverListController(
+                this,
+                mPm,
+                getTargetIntent(),
+                getReferrerPackageName(),
+                mLaunchedFromUid);
     }
 
     /**
@@ -914,32 +901,38 @@
         // to handle.
         mAdapter = createAdapter(this, payloadIntents, initialIntents, rList,
                 mLaunchedFromUid, alwaysUseOption && !isVoiceInteraction());
+        boolean rebuildCompleted = mAdapter.rebuildList();
 
-        final int layoutId;
         if (mAdapter.hasFilteredItem()) {
-            layoutId = R.layout.resolver_list_with_default;
+            mLayoutId = R.layout.resolver_list_with_default;
             alwaysUseOption = false;
         } else {
-            layoutId = getLayoutResource();
+            mLayoutId = getLayoutResource();
         }
         mAlwaysUseOption = alwaysUseOption;
 
         int count = mAdapter.getUnfilteredCount();
-        if (count == 1 && mAdapter.getOtherProfile() == null) {
-            // Only one target, so we're a candidate to auto-launch!
-            final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
-            if (shouldAutoLaunchSingleChoice(target)) {
-                safelyStartActivity(target);
-                mPackageMonitor.unregister();
-                mRegistered = false;
-                finish();
-                return true;
+
+        // 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) {
+            if (count == 1 && mAdapter.getOtherProfile() == null) {
+                // Only one target, so we're a candidate to auto-launch!
+                final TargetInfo target = mAdapter.targetInfoForPosition(0, false);
+                if (shouldAutoLaunchSingleChoice(target)) {
+                    safelyStartActivity(target);
+                    mPackageMonitor.unregister();
+                    mRegistered = false;
+                    finish();
+                    return true;
+                }
             }
         }
-        if (count > 0) {
-            setContentView(layoutId);
+
+        if (count > 0 || !rebuildCompleted) {
+            setContentView(mLayoutId);
             mAdapterView = (AbsListView) findViewById(R.id.resolver_list);
-            onPrepareAdapterView(mAdapterView, mAdapter, alwaysUseOption);
+            onPrepareAdapterView(mAdapterView, mAdapter, mAlwaysUseOption);
         } else {
             setContentView(R.layout.resolver_list);
 
@@ -1236,20 +1229,21 @@
         private final List<ResolveInfo> mBaseResolveList;
         private ResolveInfo mLastChosen;
         private DisplayResolveInfo mOtherProfile;
-        private final int mLaunchedFromUid;
         private boolean mHasExtendedInfo;
+        private ResolverListController mResolverListController;
 
         protected final LayoutInflater mInflater;
 
         List<DisplayResolveInfo> mDisplayList;
-        List<ResolvedComponentInfo> mOrigResolveList;
+        List<ResolvedComponentInfo> mUnfilteredResolveList;
 
         private int mLastChosenPosition = -1;
         private boolean mFilterLastUsed;
 
         public ResolveListAdapter(Context context, List<Intent> payloadIntents,
                 Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid,
-                boolean filterLastUsed) {
+                boolean filterLastUsed,
+                ResolverListController resolverListController) {
             mIntents = payloadIntents;
             mInitialIntents = initialIntents;
             mBaseResolveList = rList;
@@ -1257,12 +1251,11 @@
             mInflater = LayoutInflater.from(context);
             mDisplayList = new ArrayList<>();
             mFilterLastUsed = filterLastUsed;
-            rebuildList();
+            mResolverListController = resolverListController;
         }
 
         public void handlePackagesChanged() {
             rebuildList();
-            notifyDataSetChanged();
             if (getCount() == 0) {
                 // We no longer have any items...  just finish the activity.
                 finish();
@@ -1293,12 +1286,17 @@
         }
 
         public float getScore(DisplayResolveInfo target) {
-            return mResolverComparator.getScore(target.getResolvedComponentName());
+            return mResolverListController.getScore(target);
         }
 
-        private void rebuildList() {
+        /**
+         * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work
+         * to complete.
+         *
+         * @return Whether or not the list building is completed.
+         */
+        protected boolean rebuildList() {
             List<ResolvedComponentInfo> currentResolveList = null;
-
             try {
                 final Intent primaryIntent = getTargetIntent();
                 mLastChosen = AppGlobals.getPackageManager().getLastChosenActivity(
@@ -1312,84 +1310,88 @@
             mOtherProfile = null;
             mDisplayList.clear();
             if (mBaseResolveList != null) {
-                currentResolveList = mOrigResolveList = new ArrayList<>();
-                addResolveListDedupe(currentResolveList, getTargetIntent(), mBaseResolveList);
+                currentResolveList = mUnfilteredResolveList = new ArrayList<>();
+                mResolverListController.addResolveListDedupe(currentResolveList,
+                        getTargetIntent(),
+                        mBaseResolveList);
             } else {
-                final boolean shouldGetResolvedFilter = shouldGetResolvedFilter();
-                final boolean shouldGetActivityMetadata = shouldGetActivityMetadata();
-                for (int i = 0, N = mIntents.size(); i < N; i++) {
-                    final Intent intent = mIntents.get(i);
-                    final List<ResolveInfo> infos = mPm.queryIntentActivities(intent,
-                            PackageManager.MATCH_DEFAULT_ONLY
-                            | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
-                            | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0));
-                    if (infos != null) {
-                        if (currentResolveList == null) {
-                            currentResolveList = mOrigResolveList = new ArrayList<>();
-                        }
-                        addResolveListDedupe(currentResolveList, intent, infos);
-                    }
+                currentResolveList =
+                        mResolverListController.getResolversForIntent(shouldGetResolvedFilter(),
+                                shouldGetActivityMetadata(),
+                                mIntents);
+                if (currentResolveList == null) {
+                    processSortedList(currentResolveList);
+                    return true;
                 }
-
-                // Filter out any activities that the launched uid does not
-                // have permission for.
-                // Also filter out those that are suspended because they couldn't
-                // be started. We don't do this when we have an explicit
-                // list of resolved activities, because that only happens when
-                // we are being subclassed, so we can safely launch whatever
-                // they gave us.
-                if (currentResolveList != null) {
-                    for (int i=currentResolveList.size()-1; i >= 0; i--) {
-                        ActivityInfo ai = currentResolveList.get(i)
-                                .getResolveInfoAt(0).activityInfo;
-                        int granted = ActivityManager.checkComponentPermission(
-                                ai.permission, mLaunchedFromUid,
-                                ai.applicationInfo.uid, ai.exported);
-                        boolean suspended = (ai.applicationInfo.flags
-                                & ApplicationInfo.FLAG_SUSPENDED) != 0;
-                        if (granted != PackageManager.PERMISSION_GRANTED || suspended
-                                || isComponentFiltered(ai)) {
-                            // Access not allowed!
-                            if (mOrigResolveList == currentResolveList) {
-                                mOrigResolveList = new ArrayList<>(mOrigResolveList);
-                            }
-                            currentResolveList.remove(i);
-                        }
-                    }
+                List<ResolvedComponentInfo> originalList =
+                        mResolverListController.filterIneligibleActivities(currentResolveList,
+                                true);
+                if (originalList != null) {
+                    mUnfilteredResolveList = originalList;
                 }
             }
             int N;
             if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) {
-                // Only display the first matches that are either of equal
-                // priority or have asked to be default options.
-                ResolvedComponentInfo rci0 = currentResolveList.get(0);
-                ResolveInfo r0 = rci0.getResolveInfoAt(0);
-                for (int i=1; i<N; i++) {
-                    ResolveInfo ri = currentResolveList.get(i).getResolveInfoAt(0);
-                    if (DEBUG) Log.v(
-                        TAG,
-                        r0.activityInfo.name + "=" +
-                        r0.priority + "/" + r0.isDefault + " vs " +
-                        ri.activityInfo.name + "=" +
-                        ri.priority + "/" + ri.isDefault);
-                    if (r0.priority != ri.priority ||
-                        r0.isDefault != ri.isDefault) {
-                        while (i < N) {
-                            if (mOrigResolveList == currentResolveList) {
-                                mOrigResolveList = new ArrayList<>(mOrigResolveList);
-                            }
-                            currentResolveList.remove(i);
-                            N--;
-                        }
-                    }
+                // We only care about fixing the unfilteredList if the current resolve list and
+                // current resolve list are currently the same.
+                List<ResolvedComponentInfo> originalList =
+                        mResolverListController.filterLowPriority(currentResolveList,
+                                mUnfilteredResolveList == currentResolveList);
+                if (originalList != null) {
+                    mUnfilteredResolveList = originalList;
                 }
+
                 if (N > 1) {
-                    mResolverComparator.compute(currentResolveList);
-                    Collections.sort(currentResolveList, mResolverComparator);
+                    AsyncTask<List<ResolvedComponentInfo>,
+                            Void,
+                            List<ResolvedComponentInfo>> sortingTask =
+                            new AsyncTask<List<ResolvedComponentInfo>,
+                                    Void,
+                                    List<ResolvedComponentInfo>>() {
+                        @Override
+                        protected List<ResolvedComponentInfo> doInBackground(
+                                List<ResolvedComponentInfo>... params) {
+                            mResolverListController.sort(params[0]);
+                            return params[0];
+                        }
+
+                        @Override
+                        protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+                            processSortedList(sortedComponents);
+                            onPrepareAdapterView(mAdapterView, mAdapter, mAlwaysUseOption);
+                            if (mProfileView != null) {
+                                bindProfileView();
+                            }
+                        }
+                    };
+                    sortingTask.execute(currentResolveList);
+                    return false;
+                } else {
+                    processSortedList(currentResolveList);
+                    return true;
                 }
+            } else {
+                processSortedList(currentResolveList);
+                return true;
+            }
+        }
+
+        private void disableLastChosenIfNeeded() {
+            // Layout doesn't handle both profile button and last chosen
+            // so disable last chosen if profile button is present.
+            if (mOtherProfile != null && mLastChosenPosition >= 0) {
+                mLastChosenPosition = -1;
+                mFilterLastUsed = false;
+            }
+        }
+
+
+        private void processSortedList(List<ResolvedComponentInfo> sortedComponents) {
+            int N;
+            if (sortedComponents != null && (N = sortedComponents.size()) != 0) {
                 // First put the initial items at the top.
                 if (mInitialIntents != null) {
-                    for (int i=0; i<mInitialIntents.length; i++) {
+                    for (int i = 0; i < mInitialIntents.length; i++) {
                         Intent ii = mInitialIntents[i];
                         if (ii == null) {
                             continue;
@@ -1405,7 +1407,7 @@
                         UserManager userManager =
                                 (UserManager) getSystemService(Context.USER_SERVICE);
                         if (ii instanceof LabeledIntent) {
-                            LabeledIntent li = (LabeledIntent)ii;
+                            LabeledIntent li = (LabeledIntent) ii;
                             ri.resolvePackageName = li.getSourcePackage();
                             ri.labelRes = li.getLabelResource();
                             ri.nonLocalizedLabel = li.getNonLocalizedLabel();
@@ -1423,16 +1425,16 @@
 
                 // Check for applications with same name and use application name or
                 // package name if necessary
-                rci0 = currentResolveList.get(0);
-                r0 = rci0.getResolveInfoAt(0);
+                ResolvedComponentInfo rci0 = sortedComponents.get(0);
+                ResolveInfo r0 = rci0.getResolveInfoAt(0);
                 int start = 0;
-                CharSequence r0Label =  r0.loadLabel(mPm);
+                CharSequence r0Label = r0.loadLabel(mPm);
                 mHasExtendedInfo = false;
                 for (int i = 1; i < N; i++) {
                     if (r0Label == null) {
                         r0Label = r0.activityInfo.packageName;
                     }
-                    ResolvedComponentInfo rci = currentResolveList.get(i);
+                    ResolvedComponentInfo rci = sortedComponents.get(i);
                     ResolveInfo ri = rci.getResolveInfoAt(0);
                     CharSequence riLabel = ri.loadLabel(mPm);
                     if (riLabel == null) {
@@ -1441,59 +1443,19 @@
                     if (riLabel.equals(r0Label)) {
                         continue;
                     }
-                    processGroup(currentResolveList, start, (i-1), rci0, r0Label);
+                    processGroup(sortedComponents, start, (i - 1), rci0, r0Label);
                     rci0 = rci;
                     r0 = ri;
                     r0Label = riLabel;
                     start = i;
                 }
                 // Process last group
-                processGroup(currentResolveList, start, (N-1), rci0, r0Label);
+                processGroup(sortedComponents, start, (N - 1), rci0, r0Label);
             }
-
-            // Layout doesn't handle both profile button and last chosen
-            // so disable last chosen if profile button is present.
-            if (mOtherProfile != null && mLastChosenPosition >= 0) {
-                mLastChosenPosition = -1;
-                mFilterLastUsed = false;
-            }
-
+            disableLastChosenIfNeeded();
             onListRebuilt();
         }
 
-        private void addResolveListDedupe(List<ResolvedComponentInfo> into, Intent intent,
-                List<ResolveInfo> from) {
-            final int fromCount = from.size();
-            final int intoCount = into.size();
-            for (int i = 0; i < fromCount; i++) {
-                final ResolveInfo newInfo = from.get(i);
-                boolean found = false;
-                // Only loop to the end of into as it was before we started; no dupes in from.
-                for (int j = 0; j < intoCount; j++) {
-                    final ResolvedComponentInfo rci = into.get(j);
-                    if (isSameResolvedComponent(newInfo, rci)) {
-                        found = true;
-                        rci.add(intent, newInfo);
-                        break;
-                    }
-                }
-                if (!found) {
-                    final ComponentName name = new ComponentName(
-                            newInfo.activityInfo.packageName, newInfo.activityInfo.name);
-                    final ResolvedComponentInfo rci = new ResolvedComponentInfo(name,
-                            intent, newInfo);
-                    rci.setPinned(isComponentPinned(name));
-                    into.add(rci);
-                }
-            }
-        }
-
-        private boolean isSameResolvedComponent(ResolveInfo a, ResolvedComponentInfo b) {
-            final ActivityInfo ai = a.activityInfo;
-            return ai.packageName.equals(b.name.getPackageName())
-                    && ai.name.equals(b.name.getClassName());
-        }
-
         public void onListRebuilt() {
             // This space for rent
         }
@@ -1715,7 +1677,8 @@
         }
     }
 
-    static final class ResolvedComponentInfo {
+    @VisibleForTesting
+    public static final class ResolvedComponentInfo {
         public final ComponentName name;
         private boolean mPinned;
         private final List<Intent> mIntents = new ArrayList<>();
diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java
new file mode 100644
index 0000000..b91ecb6
--- /dev/null
+++ b/core/java/com/android/internal/app/ResolverListController.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.internal.app;
+
+import android.annotation.WorkerThread;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
+ * resolvers.
+ */
+public class ResolverListController {
+
+    private final Context mContext;
+    private final PackageManager mpm;
+    private final int mLaunchedFromUid;
+
+    // Needed for sorting resolvers.
+    private final Intent mTargetIntent;
+    private final String mReferrerPackage;
+
+    private static final String TAG = "ResolverListController";
+    private static final boolean DEBUG = false;
+
+    private ResolverComparator mResolverComparator;
+
+    public ResolverListController(
+            Context context,
+            PackageManager pm,
+            Intent targetIntent,
+            String referrerPackage,
+            int launchedFromUid) {
+        mContext = context;
+        mpm = pm;
+        mLaunchedFromUid = launchedFromUid;
+        mTargetIntent = targetIntent;
+        mReferrerPackage = referrerPackage;
+    }
+
+    @VisibleForTesting
+    public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent(
+            boolean shouldGetResolvedFilter,
+            boolean shouldGetActivityMetadata,
+            List<Intent> intents) {
+        List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
+        for (int i = 0, N = intents.size(); i < N; i++) {
+            final Intent intent = intents.get(i);
+            final List<ResolveInfo> infos = mpm.queryIntentActivities(intent,
+                    PackageManager.MATCH_DEFAULT_ONLY
+                            | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
+                            | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0));
+            if (infos != null) {
+                if (resolvedComponents == null) {
+                    resolvedComponents = new ArrayList<>();
+                }
+                addResolveListDedupe(resolvedComponents, intent, infos);
+            }
+        }
+        return resolvedComponents;
+    }
+
+    @VisibleForTesting
+    public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
+            Intent intent,
+            List<ResolveInfo> from) {
+        final int fromCount = from.size();
+        final int intoCount = into.size();
+        for (int i = 0; i < fromCount; i++) {
+            final ResolveInfo newInfo = from.get(i);
+            boolean found = false;
+            // Only loop to the end of into as it was before we started; no dupes in from.
+            for (int j = 0; j < intoCount; j++) {
+                final ResolverActivity.ResolvedComponentInfo rci = into.get(j);
+                if (isSameResolvedComponent(newInfo, rci)) {
+                    found = true;
+                    rci.add(intent, newInfo);
+                    break;
+                }
+            }
+            if (!found) {
+                final ComponentName name = new ComponentName(
+                        newInfo.activityInfo.packageName, newInfo.activityInfo.name);
+                final ResolverActivity.ResolvedComponentInfo rci =
+                        new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
+                rci.setPinned(isComponentPinned(name));
+                into.add(rci);
+            }
+        }
+    }
+
+    // Filter out any activities that the launched uid does not have permission for.
+    //
+    // Also filter out those that are suspended because they couldn't be started. We don't do this
+    // when we have an explicit list of resolved activities, because that only happens when
+    // we are being subclassed, so we can safely launch whatever they gave us.
+    //
+    // To preserve the inputList, optionally will return the original list if any modification has
+    // been made.
+    @VisibleForTesting
+    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
+            List<ResolverActivity.ResolvedComponentInfo> inputList,
+            boolean returnCopyOfOriginalListIfModified) {
+        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+        for (int i = inputList.size()-1; i >= 0; i--) {
+            ActivityInfo ai = inputList.get(i)
+                    .getResolveInfoAt(0).activityInfo;
+            int granted = ActivityManager.checkComponentPermission(
+                    ai.permission, mLaunchedFromUid,
+                    ai.applicationInfo.uid, ai.exported);
+            boolean suspended = (ai.applicationInfo.flags
+                    & ApplicationInfo.FLAG_SUSPENDED) != 0;
+            if (granted != PackageManager.PERMISSION_GRANTED || suspended
+                    || isComponentFiltered(ai.getComponentName())) {
+                // Access not allowed! We're about to filter an item,
+                // so modify the unfiltered version if it hasn't already been modified.
+                if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+                    listToReturn = new ArrayList<>(inputList);
+                }
+                inputList.remove(i);
+            }
+        }
+        return listToReturn;
+    }
+
+    // Filter out any low priority items.
+    //
+    // To preserve the inputList, optionally will return the original list if any modification has
+    // been made.
+    @VisibleForTesting
+    public ArrayList<ResolverActivity.ResolvedComponentInfo> filterLowPriority(
+            List<ResolverActivity.ResolvedComponentInfo> inputList,
+            boolean returnCopyOfOriginalListIfModified) {
+        ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+        // Only display the first matches that are either of equal
+        // priority or have asked to be default options.
+        ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
+        ResolveInfo r0 = rci0.getResolveInfoAt(0);
+        int N = inputList.size();
+        for (int i = 1; i < N; i++) {
+            ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
+            if (DEBUG) Log.v(
+                    TAG,
+                    r0.activityInfo.name + "=" +
+                            r0.priority + "/" + r0.isDefault + " vs " +
+                            ri.activityInfo.name + "=" +
+                            ri.priority + "/" + ri.isDefault);
+            if (r0.priority != ri.priority ||
+                    r0.isDefault != ri.isDefault) {
+                while (i < N) {
+                    if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+                        listToReturn = new ArrayList<>(inputList);
+                    }
+                    inputList.remove(i);
+                    N--;
+                }
+            }
+        }
+        return listToReturn;
+    }
+
+    @VisibleForTesting
+    @WorkerThread
+    public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
+        if (mResolverComparator == null) {
+            mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
+        }
+        mResolverComparator.compute(inputList);
+        Collections.sort(inputList, mResolverComparator);
+    }
+
+    private static boolean isSameResolvedComponent(ResolveInfo a,
+            ResolverActivity.ResolvedComponentInfo b) {
+        final ActivityInfo ai = a.activityInfo;
+        return ai.packageName.equals(b.name.getPackageName())
+                && ai.name.equals(b.name.getClassName());
+    }
+
+    boolean isComponentPinned(ComponentName name) {
+        return false;
+    }
+
+    boolean isComponentFiltered(ComponentName componentName) {
+        return false;
+    }
+
+    @VisibleForTesting
+    public float getScore(ResolverActivity.DisplayResolveInfo target) {
+        if (mResolverComparator == null) {
+            mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage);
+        }
+        return mResolverComparator.getScore(target.getResolvedComponentName());
+    }
+}
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index ba1a55d..cd41987 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -1156,6 +1156,7 @@
         </activity>
         <activity android:name="android.app.EmptyActivity">
         </activity>
+        <activity android:name="com.android.internal.app.ChooserWrapperActivity"/>
 
         <receiver android:name="android.app.activity.AbortReceiver">
             <intent-filter android:priority="1">
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
new file mode 100644
index 0000000..8a7b881
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.app;
+
+import com.android.internal.R;
+import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.action.ViewActions.click;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static com.android.internal.app.ChooserWrapperActivity.sOverrides;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Chooser activity instrumentation tests
+ */
+@RunWith(AndroidJUnit4.class)
+public class ChooserActivityTest {
+    @Rule
+    public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
+            new ActivityTestRule<>(ChooserWrapperActivity.class, false,
+                    false);
+
+    @Before
+    public void cleanOverrideData() {
+        sOverrides.reset();
+    }
+
+    @Test
+    public void customTitle() throws InterruptedException {
+        Intent sendIntent = createSendImageIntent();
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(null);
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
+        waitForIdle();
+        onView(withId(R.id.title)).check(matches(withText("chooser test")));
+    }
+
+    @Test
+    public void emptyTitle() throws InterruptedException {
+        sOverrides.isVoiceInteraction = false;
+        Intent sendIntent = createSendImageIntent();
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(null);
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
+        onView(withId(R.id.title))
+                .check(matches(withText(R.string.whichSendApplication)));
+    }
+
+    @Test
+    public void twoOptionsAndUserSelectsOne() throws InterruptedException {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+        final ChooserWrapperActivity activity = mActivityRule
+                .launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
+
+        assertThat(activity.getAdapter().getCount(), is(2));
+        onView(withId(R.id.profile_button)).check(matches(not(isDisplayed())));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+        onView(withText(toChoose.activityInfo.name))
+                .perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+    @Test
+    public void noResultsFromPackageManager() {
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(null);
+        Intent sendIntent = createSendImageIntent();
+        final ChooserWrapperActivity activity = mActivityRule
+                .launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
+        assertThat(activity.isFinishing(), is(false));
+
+        onView(withId(R.id.empty)).check(matches(isDisplayed()));
+        onView(withId(R.id.resolver_list)).check(matches(not(isDisplayed())));
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(
+                () -> activity.getAdapter().handlePackagesChanged()
+        );
+        // backward compatibility. looks like we finish when data is empty after package change
+        assertThat(activity.isFinishing(), is(true));
+    }
+
+    @Test
+    public void autoLaunchSingleResult() throws InterruptedException {
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+        Intent sendIntent = createSendImageIntent();
+        final ChooserWrapperActivity activity = mActivityRule
+                .launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
+
+        assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+        assertThat(activity.isFinishing(), is(true));
+    }
+
+    private Intent createSendImageIntent() {
+        Intent sendIntent = new Intent();
+        sendIntent.setAction(Intent.ACTION_SEND);
+        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+        sendIntent.setType("image/jpeg");
+        return sendIntent;
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            infoList.add(ChooserDataProvider.createResolvedComponentInfo(i));
+        }
+        return infoList;
+    }
+
+    private void waitForIdle() {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java b/core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java
new file mode 100644
index 0000000..f6f63f1
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java
@@ -0,0 +1,75 @@
+/*
+ * 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.internal.app;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utility class used by chooser tests to create mock data
+ */
+class ChooserDataProvider {
+
+    static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo(int i) {
+        return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
+                createResolverIntent(i), createResolveInfo(i));
+    }
+
+    static ComponentName createComponentName(int i) {
+        final String name = "component" + i;
+        return new ComponentName("foo.bar." + name, name);
+    }
+
+    static ResolveInfo createResolveInfo(int i) {
+        final ResolveInfo resolveInfo = new ResolveInfo();
+        resolveInfo.activityInfo = createActivityInfo(i);
+        resolveInfo.targetUserId = UserHandle.USER_CURRENT;
+        return resolveInfo;
+    }
+
+    static ActivityInfo createActivityInfo(int i) {
+        ActivityInfo ai = new ActivityInfo();
+        ai.name = "activity_name" + i;
+        ai.packageName = "foo_bar" + i;
+        ai.enabled = true;
+        ai.exported = true;
+        ai.permission = null;
+        ai.applicationInfo = createApplicationInfo();
+        return ai;
+    }
+
+    static ApplicationInfo createApplicationInfo() {
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.name = "app_name";
+        ai.packageName = "foo.bar";
+        ai.enabled = true;
+        return ai;
+    }
+
+    static Intent createResolverIntent(int i) {
+        return new Intent("intentAction" + i);
+    }
+}
\ No newline at end of file
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
new file mode 100644
index 0000000..66fb451
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -0,0 +1,85 @@
+/*
+ * 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.internal.app;
+
+import android.content.pm.PackageManager;
+
+import java.util.function.Function;
+
+import static org.mockito.Mockito.mock;
+
+
+/**
+ * Simple wrapper around chooser activity to be able to initiate it under test
+ */
+public class ChooserWrapperActivity extends ChooserActivity {
+    static final OverrideData sOverrides = new OverrideData();
+
+    ResolveListAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public boolean isVoiceInteraction() {
+        if (sOverrides.isVoiceInteraction != null) {
+            return sOverrides.isVoiceInteraction;
+        }
+        return super.isVoiceInteraction();
+    }
+
+    @Override
+    public void safelyStartActivity(TargetInfo cti) {
+        if (sOverrides.onSafelyStartCallback != null &&
+                sOverrides.onSafelyStartCallback.apply(cti)) {
+            return;
+        }
+        super.safelyStartActivity(cti);
+    }
+
+    @Override
+    protected ResolverListController createListController() {
+        return sOverrides.resolverListController;
+    }
+
+    @Override
+    public PackageManager getPackageManager() {
+        if (sOverrides.createPackageManager != null) {
+            return sOverrides.createPackageManager.apply(super.getPackageManager());
+        }
+        return super.getPackageManager();
+    }
+
+    /**
+     * We cannot directly mock the activity created since instrumentation creates it.
+     * <p>
+     * Instead, we use static instances of this object to modify behavior.
+     */
+    static class OverrideData {
+        @SuppressWarnings("Since15")
+        public Function<PackageManager, PackageManager> createPackageManager;
+        public Function<TargetInfo, Boolean> onSafelyStartCallback;
+        public ResolverListController resolverListController;
+        public Boolean isVoiceInteraction;
+
+        public void reset() {
+            onSafelyStartCallback = null;
+            isVoiceInteraction = null;
+            createPackageManager = null;
+            resolverListController = mock(ResolverListController.class);
+        }
+    }
+}
\ No newline at end of file