Merge "Do the sorting for the ShareSheet asynchronously."

This commit is contained in:
Hakan Seyalioglu
2017-01-03 23:33:44 +00:00
committed by Android (Google) Code Review
7 changed files with 764 additions and 200 deletions

View File

@@ -69,6 +69,7 @@ import android.widget.AbsListView;
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 @@ public class ChooserActivity extends ResolverActivity {
private IntentSender mRefinementIntentSender;
private RefinementResultReceiver mRefinementResultReceiver;
private ChooserTarget[] mCallerChooserTargets;
private ComponentName[] mFilteredComponentNames;
private Intent mReferrerFillInIntent;
@@ -235,7 +237,7 @@ public class ChooserActivity extends ResolverActivity {
}
names[i] = (ComponentName) pa[i];
}
setFilteredComponents(names);
mFilteredComponentNames = names;
}
pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
@@ -642,17 +644,65 @@ public class ChooserActivity extends ResolverActivity {
}
}
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 class ChooserActivity extends ResolverActivity {
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();
@@ -921,18 +972,6 @@ public class ChooserActivity extends ResolverActivity {
return mPinnedSharedPrefs.getBoolean(name.flattenToString(), 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 View onCreateView(ViewGroup parent) {
return mInflater.inflate(
@@ -944,6 +983,8 @@ public class ChooserActivity extends ResolverActivity {
if (mServiceTargets != null) {
pruneServiceTargets();
}
if (DEBUG) Log.d(TAG, "List built querying services");
queryTargetServices(this);
}
@Override

View File

@@ -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.text.TextUtils;
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 com.android.internal.widget.ResolverDrawerLayout;
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.Objects;
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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
mPackageMonitor.register(this, getMainLooper(), false);
mRegistered = true;
mReferrerPackage = getReferrerPackageName();
final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
mIconDpi = am.getLauncherLargeIconDensity();
@@ -268,11 +268,6 @@ public class ResolverActivity extends Activity {
// 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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
+ (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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
}
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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
// 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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
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 class ResolverActivity extends Activity {
}
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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
// 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 @@ public class ResolverActivity extends Activity {
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 @@ public class ResolverActivity extends Activity {
}
}
static final class ResolvedComponentInfo {
@VisibleForTesting
public static final class ResolvedComponentInfo {
public final ComponentName name;
private boolean mPinned;
private final List<Intent> mIntents = new ArrayList<>();

View File

@@ -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());
}
}

View File

@@ -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">

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}