From e1276bfe086a40a0fba7e81dd5db13f95287714d Mon Sep 17 00:00:00 2001 From: Hakan Seyalioglu Date: Wed, 7 Dec 2016 16:38:57 -0800 Subject: [PATCH] Do the sorting for the ShareSheet asynchronously. This lets the screen fade out and share sheet start being displayed before all results are sorted. Based heavily on ag/1126533. There are two other follow ups that I'd like to do after this that aren't a part of this change: 1) Investigating images being loaded multiple times (from original change) 2) Put placeholder space while the sorted images are coming in - at the moment there's some jank when the list of applications becomes visible. Test: Unit tests for ChooserActivityTest (still would like to add tests for ResolveInfoController and UsbResolverActivity). Change-Id: I4c4a65fff1b0f17fc52b9bd01c3d5c59d649d119 --- .../android/internal/app/ChooserActivity.java | 77 ++++- .../internal/app/ResolverActivity.java | 327 ++++++++---------- .../internal/app/ResolverListController.java | 221 ++++++++++++ core/tests/coretests/AndroidManifest.xml | 1 + .../internal/app/ChooserActivityTest.java | 178 ++++++++++ .../internal/app/ChooserDataProvider.java | 75 ++++ .../internal/app/ChooserWrapperActivity.java | 85 +++++ 7 files changed, 764 insertions(+), 200 deletions(-) create mode 100644 core/java/com/android/internal/app/ResolverListController.java create mode 100644 core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java create mode 100644 core/tests/coretests/src/com/android/internal/app/ChooserDataProvider.java create mode 100644 core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index c314cae2f55c7..cb7be2e2e07fe 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.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 payloadIntents, Intent[] initialIntents, List 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 payloadIntents, Intent[] initialIntents, List 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 diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index c516b5cd7c3ad..7c22c4fac9b5e 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.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 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 bestMatch) bestMatch = r.match; @@ -899,7 +876,17 @@ public class ResolverActivity extends Activity { Intent[] initialIntents, List 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 mBaseResolveList; private ResolveInfo mLastChosen; private DisplayResolveInfo mOtherProfile; - private final int mLaunchedFromUid; private boolean mHasExtendedInfo; + private ResolverListController mResolverListController; protected final LayoutInflater mInflater; List mDisplayList; - List mOrigResolveList; + List mUnfilteredResolveList; private int mLastChosenPosition = -1; private boolean mFilterLastUsed; public ResolveListAdapter(Context context, List payloadIntents, Intent[] initialIntents, List 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 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 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 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(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 originalList = + mResolverListController.filterLowPriority(currentResolveList, + mUnfilteredResolveList == currentResolveList); + if (originalList != null) { + mUnfilteredResolveList = originalList; } + if (N > 1) { - mResolverComparator.compute(currentResolveList); - Collections.sort(currentResolveList, mResolverComparator); + AsyncTask, + Void, + List> sortingTask = + new AsyncTask, + Void, + List>() { + @Override + protected List doInBackground( + List... params) { + mResolverListController.sort(params[0]); + return params[0]; + } + + @Override + protected void onPostExecute(List 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 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= 0) { - mLastChosenPosition = -1; - mFilterLastUsed = false; - } - + disableLastChosenIfNeeded(); onListRebuilt(); } - private void addResolveListDedupe(List into, Intent intent, - List 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 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 0000000000000..b91ecb625bcf5 --- /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 getResolversForIntent( + boolean shouldGetResolvedFilter, + boolean shouldGetActivityMetadata, + List intents) { + List resolvedComponents = null; + for (int i = 0, N = intents.size(); i < N; i++) { + final Intent intent = intents.get(i); + final List 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 into, + Intent intent, + List 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 filterIneligibleActivities( + List inputList, + boolean returnCopyOfOriginalListIfModified) { + ArrayList 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 filterLowPriority( + List inputList, + boolean returnCopyOfOriginalListIfModified) { + ArrayList 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 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 ba1a55d7f9a6f..cd419878bb501 100644 --- a/core/tests/coretests/AndroidManifest.xml +++ b/core/tests/coretests/AndroidManifest.xml @@ -1156,6 +1156,7 @@ + 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 0000000000000..8a7b881e4ae87 --- /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 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 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 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 createResolvedComponentsForTest(int numberOfResults) { + List 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 0000000000000..f6f63f1de81e1 --- /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 0000000000000..66fb45148a6e7 --- /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. + *

+ * Instead, we use static instances of this object to modify behavior. + */ + static class OverrideData { + @SuppressWarnings("Since15") + public Function createPackageManager; + public Function 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