Merge "Do the sorting for the ShareSheet asynchronously."
This commit is contained in:
committed by
Android (Google) Code Review
commit
e5167e3f8f
@@ -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
|
||||
|
||||
@@ -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<>();
|
||||
|
||||
221
core/java/com/android/internal/app/ResolverListController.java
Normal file
221
core/java/com/android/internal/app/ResolverListController.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user