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