From b0802dc1fceb3da127a4e5c1193fc2a83d3db4a3 Mon Sep 17 00:00:00 2001 From: arangelov Date: Fri, 18 Oct 2019 18:03:44 +0100 Subject: [PATCH] Separate adapter logic from ChooserActivity and ResolverActivity Test: atest com.android.internal.app.ChooserActivityTest Test: atest com.android.internal.app.ResolverActivityTest Bug: 142537267 Bug: 142538125 Change-Id: Ia4a61e3dc7cbac5c31e0d5b126e033a043c81d90 --- .../android/internal/app/ChooserActivity.java | 1008 ++------------- .../android/internal/app/ChooserFlags.java | 38 + .../internal/app/ChooserListAdapter.java | 539 ++++++++ .../internal/app/ResolverActivity.java | 1148 ++--------------- .../internal/app/ResolverListAdapter.java | 862 +++++++++++++ .../internal/app/ResolverListController.java | 3 +- .../internal/app/SimpleIconFactory.java | 2 +- .../app/chooser/ChooserTargetInfo.java | 53 + .../app/chooser/DisplayResolveInfo.java | 182 +++ .../app/chooser/NotSelectableTargetInfo.java | 88 ++ .../app/chooser/SelectableTargetInfo.java | 316 +++++ .../internal/app/chooser/TargetInfo.java | 128 ++ .../internal/app/ChooserActivityTest.java | 25 +- .../internal/app/ChooserWrapperActivity.java | 12 +- .../internal/app/ResolverActivityTest.java | 12 +- .../internal/app/ResolverWrapperActivity.java | 43 +- .../internal/app/ResolverWrapperAdapter.java | 71 + .../systemui/usb/UsbResolverActivity.java | 1 + 18 files changed, 2542 insertions(+), 1989 deletions(-) create mode 100644 core/java/com/android/internal/app/ChooserFlags.java create mode 100644 core/java/com/android/internal/app/ChooserListAdapter.java create mode 100644 core/java/com/android/internal/app/ResolverListAdapter.java create mode 100644 core/java/com/android/internal/app/chooser/ChooserTargetInfo.java create mode 100644 core/java/com/android/internal/app/chooser/DisplayResolveInfo.java create mode 100644 core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java create mode 100644 core/java/com/android/internal/app/chooser/SelectableTargetInfo.java create mode 100644 core/java/com/android/internal/app/chooser/TargetInfo.java create mode 100644 core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 3b82f186d80e9..b1752a47ea939 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -45,8 +45,6 @@ import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.LabeledIntent; -import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; @@ -61,9 +59,7 @@ import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.drawable.AnimatedVectorDrawable; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; import android.metrics.LogMaker; import android.net.Uri; import android.os.AsyncTask; @@ -85,7 +81,6 @@ import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; import android.service.chooser.IChooserTargetResult; import android.service.chooser.IChooserTargetService; -import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.HashedStringCache; @@ -110,6 +105,14 @@ import android.widget.Toast; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; +import com.android.internal.app.ResolverListAdapter.ViewHolder; +import com.android.internal.app.chooser.ChooserTargetInfo; +import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.NotSelectableTargetInfo; +import com.android.internal.app.chooser.SelectableTargetInfo; +import com.android.internal.app.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator; +import com.android.internal.app.chooser.TargetInfo; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; @@ -124,7 +127,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.Collator; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; @@ -138,7 +140,9 @@ import java.util.Set; * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence). * */ -public class ChooserActivity extends ResolverActivity { +public class ChooserActivity extends ResolverActivity implements + ChooserListAdapter.ChooserListCommunicator, + SelectableTargetInfoCommunicator { private static final String TAG = "ChooserActivity"; @@ -154,12 +158,6 @@ public class ChooserActivity extends ResolverActivity { private static final boolean DEBUG = false; - /** - * If {@link #USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true, - * {@link AppPredictionManager} will be queried for direct share targets. - */ - // TODO(b/123089490): Replace with system flag - private static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true; private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true; // TODO(b/123088566) Share these in a better way. private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share"; @@ -167,24 +165,21 @@ public class ChooserActivity extends ResolverActivity { private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20; public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"; + @VisibleForTesting + public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250; + private boolean mIsAppPredictorComponentAvailable; private AppPredictor mAppPredictor; private AppPredictor.Callback mAppPredictorCallback; private Map mDirectShareAppTargetCache; - /** - * If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of - * binding to every ChooserTargetService implementation. - */ - // TODO(b/121287573): Replace with a system flag (setprop?) - private static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true; - private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true; - public static final int TARGET_TYPE_DEFAULT = 0; public static final int TARGET_TYPE_CHOOSER_TARGET = 1; public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2; public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3; + private static final boolean USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS = true; + @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = { TARGET_TYPE_DEFAULT, TARGET_TYPE_CHOOSER_TARGET, @@ -233,10 +228,6 @@ public class ChooserActivity extends ResolverActivity { private int mCurrAvailableWidth = 0; - /** {@link ChooserActivity#getBaseScore} */ - public static final float CALLER_TARGET_SCORE_BOOST = 900.f; - /** {@link ChooserActivity#getBaseScore} */ - public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; // TODO: Update to handle landscape instead of using static value private static final int MAX_RANKED_TARGETS = 4; @@ -246,14 +237,9 @@ public class ChooserActivity extends ResolverActivity { private static final int MAX_LOG_RANK_POSITION = 12; - @VisibleForTesting - public static final int LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS = 250; - private static final int MAX_EXTRA_INITIAL_INTENTS = 2; private static final int MAX_EXTRA_CHOOSER_TARGETS = 2; - private boolean mListViewDataChanged = false; - @Retention(SOURCE) @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT}) private @interface ContentPreviewType { @@ -266,9 +252,6 @@ public class ChooserActivity extends ResolverActivity { private static final int CONTENT_PREVIEW_TEXT = 3; protected MetricsLogger mMetricsLogger; - // Sorted list of DisplayResolveInfos for the alphabetical app section. - private List mSortedList = new ArrayList<>(); - private ContentPreviewCoordinator mPreviewCoord; private class ContentPreviewCoordinator { @@ -645,8 +628,7 @@ public class ChooserActivity extends ResolverActivity { if (isFinishing() || isDestroyed()) { return; } - // May be null if there are no apps to perform share/open action. - if (mChooserListAdapter == null) { + if (mChooserListAdapter.getCount() == 0) { return; } if (resultList.isEmpty()) { @@ -775,7 +757,7 @@ public class ChooserActivity extends ResolverActivity { @Override public void onSomePackagesChanged() { mAdapter.handlePackagesChanged(); - bindProfileView(); + updateProfileViewButton(); } }; } @@ -1191,7 +1173,7 @@ public class ChooserActivity extends ResolverActivity { } } - @Override + @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { Intent result = defIntent; if (mReplacementExtras != null) { @@ -1231,9 +1213,8 @@ public class ChooserActivity extends ResolverActivity { } @Override - public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { + public void onPrepareAdapterView(AbsListView adapterView, ResolverListAdapter adapter) { final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; - mChooserListAdapter = (ChooserListAdapter) adapter; if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) { mChooserListAdapter.addServiceResults(null, Lists.newArrayList(mCallerChooserTargets), TARGET_TYPE_DEFAULT); @@ -1244,12 +1225,18 @@ public class ChooserActivity extends ResolverActivity { } } + @Override + protected boolean rebuildList() { + mChooserListAdapter = (ChooserListAdapter) mAdapter; + return rebuildListInternal(); + } + @Override public int getLayoutResource() { return R.layout.chooser_grid; } - @Override + @Override // ResolverListCommunicator public boolean shouldGetActivityMetadata() { return true; } @@ -1328,7 +1315,7 @@ public class ChooserActivity extends ResolverActivity { final long selectionCost = System.currentTimeMillis() - mChooserShownTime; super.startSelected(which, always, filtered); - if (mChooserListAdapter != null) { + if (mChooserListAdapter.getCount() > 0) { // Log the index of which type of target the user picked. // Lower values mean the ranking was better. int cat = 0; @@ -1342,7 +1329,7 @@ public class ChooserActivity extends ResolverActivity { // Log the package name + target name to answer the question if most users // share to mostly the same person or to a bunch of different people. ChooserTarget target = - mChooserListAdapter.mServiceTargets.get(value).getChooserTarget(); + mChooserListAdapter.getChooserTargetForValue(value); directTargetHashed = HashedStringCache.getInstance().hashString( this, TAG, @@ -1428,7 +1415,7 @@ public class ChooserActivity extends ResolverActivity { continue; } final ActivityInfo ai = dri.getResolveInfo().activityInfo; - if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS + if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS && sm.hasShareTargets(ai.packageName)) { // Share targets will be queried from ShortcutManager continue; @@ -1817,8 +1804,8 @@ public class ChooserActivity extends ResolverActivity { */ @Nullable private AppPredictor getAppPredictorForDirectShareIfEnabled() { - return USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS && !ActivityManager.isLowRamDeviceStatic() - ? getAppPredictor() : null; + return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS + && !ActivityManager.isLowRamDeviceStatic() ? getAppPredictor() : null; } /** @@ -1900,24 +1887,18 @@ public class ChooserActivity extends ResolverActivity { } } - private void updateAlphabeticalList() { - mSortedList.clear(); - mSortedList.addAll(getDisplayList()); - Collections.sort(mSortedList, new AzInfoComparator(ChooserActivity.this)); - } - /** * Sort intents alphabetically based on display label. */ - class AzInfoComparator implements Comparator { + static class AzInfoComparator implements Comparator { Collator mCollator; AzInfoComparator(Context context) { mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); } @Override - public int compare(ResolverActivity.DisplayResolveInfo lhsp, - ResolverActivity.DisplayResolveInfo rhsp) { + public int compare( + DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel()); } } @@ -1955,12 +1936,12 @@ public class ChooserActivity extends ResolverActivity { } @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, createListController()); - return adapter; + public ResolverListAdapter createAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, + boolean filterLastUsed, boolean useLayoutForBrowsables) { + return new ChooserListAdapter(context, payloadIntents, + initialIntents, rList, filterLastUsed, createListController(), + useLayoutForBrowsables, this, this); } @VisibleForTesting @@ -1999,347 +1980,21 @@ public class ChooserActivity extends ResolverActivity { return null; } - interface ChooserTargetInfo extends TargetInfo { - float getModifiedScore(); - - ChooserTarget getChooserTarget(); - - /** - * Do not label as 'equals', since this doesn't quite work - * as intended with java 8. - */ - default boolean isSimilar(ChooserTargetInfo other) { - if (other == null) return false; - - ChooserTarget ct1 = getChooserTarget(); - ChooserTarget ct2 = other.getChooserTarget(); - - // If either is null, there is not enough info to make an informed decision - // about equality, so just exit - if (ct1 == null || ct2 == null) return false; - - if (ct1.getComponentName().equals(ct2.getComponentName()) - && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel()) - && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) { - return true; - } - - return false; - } - } - - /** - * Distinguish between targets that selectable by the user, vs those that are - * placeholders for the system while information is loading in an async manner. - */ - abstract class NotSelectableTargetInfo implements ChooserTargetInfo { - - public Intent getResolvedIntent() { - return null; - } - - public ComponentName getResolvedComponentName() { - return null; - } - - public boolean start(Activity activity, Bundle options) { - return false; - } - - public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { - return false; - } - - public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - return false; - } - - public ResolveInfo getResolveInfo() { - return null; - } - - public CharSequence getDisplayLabel() { - return null; - } - - public CharSequence getExtendedInfo() { - return null; - } - - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return null; - } - - public List getAllSourceIntents() { - return null; - } - - public float getModifiedScore() { - return -0.1f; - } - - public ChooserTarget getChooserTarget() { - return null; - } - - public boolean isSuspended() { - return false; - } - } - - final class PlaceHolderTargetInfo extends NotSelectableTargetInfo { - public Drawable getDisplayIcon() { + static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo { + public Drawable getDisplayIcon(Context context) { AnimatedVectorDrawable avd = (AnimatedVectorDrawable) - getDrawable(R.drawable.chooser_direct_share_icon_placeholder); + context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder); avd.start(); // Start animation after generation return avd; } } - - final class EmptyTargetInfo extends NotSelectableTargetInfo { - public Drawable getDisplayIcon() { + static final class EmptyTargetInfo extends NotSelectableTargetInfo { + public Drawable getDisplayIcon(Context context) { return null; } } - final class SelectableTargetInfo implements ChooserTargetInfo { - private final DisplayResolveInfo mSourceInfo; - private final ResolveInfo mBackupResolveInfo; - private final ChooserTarget mChooserTarget; - private final String mDisplayLabel; - private Drawable mBadgeIcon = null; - private CharSequence mBadgeContentDescription; - private Drawable mDisplayIcon; - private final Intent mFillInIntent; - private final int mFillInFlags; - private final float mModifiedScore; - private boolean mIsSuspended = false; - - SelectableTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, - float modifiedScore) { - mSourceInfo = sourceInfo; - mChooserTarget = chooserTarget; - mModifiedScore = modifiedScore; - if (sourceInfo != null) { - final ResolveInfo ri = sourceInfo.getResolveInfo(); - if (ri != null) { - final ActivityInfo ai = ri.activityInfo; - if (ai != null && ai.applicationInfo != null) { - final PackageManager pm = getPackageManager(); - mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); - mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); - mIsSuspended = - (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; - } - } - } - // TODO(b/121287224): do this in the background thread, and only for selected targets - mDisplayIcon = getChooserTargetIconDrawable(chooserTarget); - - if (sourceInfo != null) { - mBackupResolveInfo = null; - } else { - mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0); - } - - mFillInIntent = null; - mFillInFlags = 0; - - mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle()); - } - - private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) { - mSourceInfo = other.mSourceInfo; - mBackupResolveInfo = other.mBackupResolveInfo; - mChooserTarget = other.mChooserTarget; - mBadgeIcon = other.mBadgeIcon; - mBadgeContentDescription = other.mBadgeContentDescription; - mDisplayIcon = other.mDisplayIcon; - mFillInIntent = fillInIntent; - mFillInFlags = flags; - mModifiedScore = other.mModifiedScore; - - mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle()); - } - - private String sanitizeDisplayLabel(CharSequence label) { - SpannableStringBuilder sb = new SpannableStringBuilder(label); - sb.clearSpans(); - return sb.toString(); - } - - public boolean isSuspended() { - return mIsSuspended; - } - - /** - * Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip - * the call to LauncherApps#getShortcuts(ShortcutQuery). - */ - // TODO(121287224): Refactor code to apply the suggestion above - private Drawable getChooserTargetIconDrawable(ChooserTarget target) { - Drawable directShareIcon = null; - - // First get the target drawable and associated activity info - final Icon icon = target.getIcon(); - if (icon != null) { - directShareIcon = icon.loadDrawable(ChooserActivity.this); - } else if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) { - Bundle extras = target.getIntentExtras(); - if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) { - CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID); - LauncherApps launcherApps = (LauncherApps) getSystemService( - Context.LAUNCHER_APPS_SERVICE); - final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery(); - q.setPackage(target.getComponentName().getPackageName()); - q.setShortcutIds(Arrays.asList(shortcutId.toString())); - q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC); - final List shortcuts = launcherApps.getShortcuts(q, getUser()); - if (shortcuts != null && shortcuts.size() > 0) { - directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0); - } - } - } - - if (directShareIcon == null) return null; - - ActivityInfo info = null; - try { - info = mPm.getActivityInfo(target.getComponentName(), 0); - } catch (NameNotFoundException error) { - Log.e(TAG, "Could not find activity associated with ChooserTarget"); - } - - if (info == null) return null; - - // Now fetch app icon and raster with no badging even in work profile - Bitmap appIcon = makePresentationGetter(info).getIconBitmap( - UserHandle.getUserHandleForUid(UserHandle.myUserId())); - - // Raster target drawable with appIcon as a badge - SimpleIconFactory sif = SimpleIconFactory.obtain(ChooserActivity.this); - Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); - sif.recycle(); - - return new BitmapDrawable(getResources(), directShareBadgedIcon); - } - - public float getModifiedScore() { - return mModifiedScore; - } - - @Override - public Intent getResolvedIntent() { - if (mSourceInfo != null) { - return mSourceInfo.getResolvedIntent(); - } - - final Intent targetIntent = new Intent(getTargetIntent()); - targetIntent.setComponent(mChooserTarget.getComponentName()); - targetIntent.putExtras(mChooserTarget.getIntentExtras()); - return targetIntent; - } - - @Override - public ComponentName getResolvedComponentName() { - if (mSourceInfo != null) { - return mSourceInfo.getResolvedComponentName(); - } else if (mBackupResolveInfo != null) { - return new ComponentName(mBackupResolveInfo.activityInfo.packageName, - mBackupResolveInfo.activityInfo.name); - } - return null; - } - - private Intent getBaseIntentToSend() { - Intent result = getResolvedIntent(); - if (result == null) { - Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); - } else { - result = new Intent(result); - if (mFillInIntent != null) { - result.fillIn(mFillInIntent, mFillInFlags); - } - result.fillIn(mReferrerFillInIntent, 0); - } - return result; - } - - @Override - public boolean start(Activity activity, Bundle options) { - throw new RuntimeException("ChooserTargets should be started as caller."); - } - - @Override - public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { - final Intent intent = getBaseIntentToSend(); - if (intent == null) { - return false; - } - intent.setComponent(mChooserTarget.getComponentName()); - intent.putExtras(mChooserTarget.getIntentExtras()); - - // Important: we will ignore the target security checks in ActivityManager - // if and only if the ChooserTarget's target package is the same package - // where we got the ChooserTargetService that provided it. This lets a - // ChooserTargetService provide a non-exported or permission-guarded target - // to the chooser for the user to pick. - // - // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere - // so we'll obey the caller's normal security checks. - final boolean ignoreTargetSecurity = mSourceInfo != null - && mSourceInfo.getResolvedComponentName().getPackageName() - .equals(mChooserTarget.getComponentName().getPackageName()); - return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId); - } - - @Override - public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - throw new RuntimeException("ChooserTargets should be started as caller."); - } - - @Override - public ResolveInfo getResolveInfo() { - return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; - } - - @Override - public CharSequence getDisplayLabel() { - return mDisplayLabel; - } - - @Override - public CharSequence getExtendedInfo() { - // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. - return null; - } - - @Override - public Drawable getDisplayIcon() { - return mDisplayIcon; - } - - public ChooserTarget getChooserTarget() { - return mChooserTarget; - } - - @Override - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new SelectableTargetInfo(this, fillInIntent, flags); - } - - @Override - public List getAllSourceIntents() { - final List results = new ArrayList<>(); - if (mSourceInfo != null) { - // We only queried the service for the first one in our sourceinfo. - results.add(mSourceInfo.getAllSourceIntents().get(0)); - } - return results; - } - } - private void handleScroll(View view, int x, int y, int oldx, int oldy) { if (mChooserRowAdapter != null) { mChooserRowAdapter.handleScroll(view, y, oldy); @@ -2408,7 +2063,8 @@ public class ChooserActivity extends ResolverActivity { boolean isExpandable = getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode(); - if (directShareHeight != 0 && isSendAction(getTargetIntent()) && isExpandable) { + if (directShareHeight != 0 && isSendAction(getTargetIntent()) + && isExpandable) { // make sure to leave room for direct share 4->8 expansion int requiredExpansionHeight = (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE); @@ -2424,498 +2080,6 @@ public class ChooserActivity extends ResolverActivity { } } - public class ChooserListAdapter extends ResolveListAdapter { - public static final int TARGET_BAD = -1; - public static final int TARGET_CALLER = 0; - public static final int TARGET_SERVICE = 1; - public static final int TARGET_STANDARD = 2; - public static final int TARGET_STANDARD_AZ = 3; - - private static final int MAX_SUGGESTED_APP_TARGETS = 4; - private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; - - private static final int MAX_SERVICE_TARGETS = 8; - - private final int mMaxShortcutTargetsPerApp = - getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp); - - private int mNumShortcutResults = 0; - - // Reserve spots for incoming direct share targets by adding placeholders - private ChooserTargetInfo mPlaceHolderTargetInfo = new PlaceHolderTargetInfo(); - private final List mServiceTargets = new ArrayList<>(); - private final List mCallerTargets = new ArrayList<>(); - - private final BaseChooserTargetComparator mBaseTargetComparator - = new BaseChooserTargetComparator(); - - public ChooserListAdapter(Context context, List payloadIntents, - Intent[] initialIntents, List rList, int launchedFromUid, - 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, - resolverListController); - - createPlaceHolders(); - - if (initialIntents != null) { - final PackageManager pm = getPackageManager(); - for (int i = 0; i < initialIntents.length; i++) { - final Intent ii = initialIntents[i]; - if (ii == null) { - continue; - } - - // We reimplement Intent#resolveActivityInfo here because if we have an - // implicit intent, we want the ResolveInfo returned by PackageManager - // instead of one we reconstruct ourselves. The ResolveInfo returned might - // have extra metadata and resolvePackageName set and we want to respect that. - ResolveInfo ri = null; - ActivityInfo ai = null; - final ComponentName cn = ii.getComponent(); - if (cn != null) { - try { - ai = pm.getActivityInfo(ii.getComponent(), 0); - ri = new ResolveInfo(); - ri.activityInfo = ai; - } catch (PackageManager.NameNotFoundException ignored) { - // ai will == null below - } - } - if (ai == null) { - ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY); - ai = ri != null ? ri.activityInfo : null; - } - if (ai == null) { - Log.w(TAG, "No activity found for " + ii); - continue; - } - UserManager userManager = - (UserManager) getSystemService(Context.USER_SERVICE); - if (ii instanceof LabeledIntent) { - LabeledIntent li = (LabeledIntent) ii; - ri.resolvePackageName = li.getSourcePackage(); - ri.labelRes = li.getLabelResource(); - ri.nonLocalizedLabel = li.getNonLocalizedLabel(); - ri.icon = li.getIconResource(); - ri.iconResourceId = ri.icon; - } - if (userManager.isManagedProfile()) { - ri.noResourceId = true; - ri.icon = 0; - } - mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii)); - } - } - } - - @Override - public void handlePackagesChanged() { - if (DEBUG) { - Log.d(TAG, "clearing queryTargets on package change"); - } - createPlaceHolders(); - mServicesRequested.clear(); - notifyDataSetChanged(); - - super.handlePackagesChanged(); - } - - @Override - public void notifyDataSetChanged() { - if (!mListViewDataChanged) { - mChooserHandler.sendEmptyMessageDelayed(ChooserHandler.LIST_VIEW_UPDATE_MESSAGE, - LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); - mListViewDataChanged = true; - } - } - - private void refreshListView() { - if (mListViewDataChanged) { - super.notifyDataSetChanged(); - } - mListViewDataChanged = false; - } - - - private void createPlaceHolders() { - mNumShortcutResults = 0; - mServiceTargets.clear(); - for (int i = 0; i < MAX_SERVICE_TARGETS; i++) { - mServiceTargets.add(mPlaceHolderTargetInfo); - } - } - - @Override - public View onCreateView(ViewGroup parent) { - return mInflater.inflate( - com.android.internal.R.layout.resolve_grid_item, parent, false); - } - - @Override - protected void onBindView(View view, TargetInfo info) { - super.onBindView(view, info); - - // If target is loading, show a special placeholder shape in the label, make unclickable - final ViewHolder holder = (ViewHolder) view.getTag(); - if (info instanceof PlaceHolderTargetInfo) { - final int maxWidth = getResources().getDimensionPixelSize( - R.dimen.chooser_direct_share_label_placeholder_max_width); - holder.text.setMaxWidth(maxWidth); - holder.text.setBackground(getResources().getDrawable( - R.drawable.chooser_direct_share_label_placeholder, getTheme())); - // Prevent rippling by removing background containing ripple - holder.itemView.setBackground(null); - } else { - holder.text.setMaxWidth(Integer.MAX_VALUE); - holder.text.setBackground(null); - holder.itemView.setBackground(holder.defaultItemViewBackground); - } - } - - /** - * Rather than fully sorting the input list, this sorting task will put the top k elements - * in the head of input list and fill the tail with other elements in undetermined order. - */ - @Override - AsyncTask, - Void, - List> createSortingTask() { - return new AsyncTask, - Void, - List>() { - @Override - protected List doInBackground( - List... params) { - mResolverListController.topK(params[0], - getMaxRankedTargets()); - return params[0]; - } - - @Override - protected void onPostExecute(List sortedComponents) { - processSortedList(sortedComponents); - bindProfileView(); - notifyDataSetChanged(); - } - }; - } - - @Override - public void onListRebuilt() { - if (getDisplayList() == null || getDisplayList().isEmpty()) { - notifyDataSetChanged(); - } else { - new AsyncTask() { - @Override - protected Void doInBackground(Void... voids) { - updateAlphabeticalList(); - return null; - } - - @Override - protected void onPostExecute(Void aVoid) { - notifyDataSetChanged(); - } - }.execute(); - } - - // don't support direct share on low ram devices - if (ActivityManager.isLowRamDeviceStatic()) { - return; - } - - if (USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS - || USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) { - if (DEBUG) { - Log.d(TAG, "querying direct share targets from ShortcutManager"); - } - - queryDirectShareTargets(this, false); - } - if (USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS) { - if (DEBUG) { - Log.d(TAG, "List built querying services"); - } - - queryTargetServices(this); - } - } - - @Override - public boolean shouldGetResolvedFilter() { - return true; - } - - @Override - public int getCount() { - return getRankedTargetCount() + getAlphaTargetCount() - + getSelectableServiceTargetCount() + getCallerTargetCount(); - } - - @Override - public int getUnfilteredCount() { - int appTargets = super.getUnfilteredCount(); - if (appTargets > getMaxRankedTargets()) { - appTargets = appTargets + getMaxRankedTargets(); - } - return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); - } - - - public int getCallerTargetCount() { - return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS); - } - - /** - * Filter out placeholders and non-selectable service targets - */ - public int getSelectableServiceTargetCount() { - int count = 0; - for (ChooserTargetInfo info : mServiceTargets) { - if (info instanceof SelectableTargetInfo) { - count++; - } - } - return count; - } - - public int getServiceTargetCount() { - if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) { - return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); - } - - return 0; - } - - int getAlphaTargetCount() { - int standardCount = super.getCount(); - return standardCount > getMaxRankedTargets() ? standardCount : 0; - } - - int getRankedTargetCount() { - int spacesAvailable = getMaxRankedTargets() - getCallerTargetCount(); - return Math.min(spacesAvailable, super.getCount()); - } - - private int getMaxRankedTargets() { - return mChooserRowAdapter == null ? 4 : mChooserRowAdapter.getMaxTargetsPerRow(); - } - - public int getPositionTargetType(int position) { - int offset = 0; - - final int serviceTargetCount = getServiceTargetCount(); - if (position < serviceTargetCount) { - return TARGET_SERVICE; - } - offset += serviceTargetCount; - - final int callerTargetCount = getCallerTargetCount(); - if (position - offset < callerTargetCount) { - return TARGET_CALLER; - } - offset += callerTargetCount; - - final int rankedTargetCount = getRankedTargetCount(); - if (position - offset < rankedTargetCount) { - return TARGET_STANDARD; - } - offset += rankedTargetCount; - - final int standardTargetCount = getAlphaTargetCount(); - if (position - offset < standardTargetCount) { - return TARGET_STANDARD_AZ; - } - - return TARGET_BAD; - } - - @Override - public TargetInfo getItem(int position) { - return targetInfoForPosition(position, true); - } - - - /** - * Find target info for a given position. - * Since ChooserActivity displays several sections of content, determine which - * section provides this item. - */ - @Override - public TargetInfo targetInfoForPosition(int position, boolean filtered) { - int offset = 0; - - // Direct share targets - final int serviceTargetCount = filtered ? getServiceTargetCount() : - getSelectableServiceTargetCount(); - if (position < serviceTargetCount) { - return mServiceTargets.get(position); - } - offset += serviceTargetCount; - - // Targets provided by calling app - final int callerTargetCount = getCallerTargetCount(); - if (position - offset < callerTargetCount) { - return mCallerTargets.get(position - offset); - } - offset += callerTargetCount; - - // Ranked standard app targets - final int rankedTargetCount = getRankedTargetCount(); - if (position - offset < rankedTargetCount) { - return filtered ? super.getItem(position - offset) - : getDisplayResolveInfo(position - offset); - } - offset += rankedTargetCount; - - // Alphabetical complete app target list. - if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { - return mSortedList.get(position - offset); - } - - return null; - } - - - /** - * Evaluate targets for inclusion in the direct share area. May not be included - * if score is too low. - */ - public void addServiceResults(DisplayResolveInfo origTarget, List targets, - @ShareTargetType int targetType) { - if (DEBUG) { - Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() - + " targets"); - } - - if (targets.size() == 0) { - return; - } - - final float baseScore = getBaseScore(origTarget, targetType); - Collections.sort(targets, mBaseTargetComparator); - - final boolean isShortcutResult = - (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER - || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); - final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp - : MAX_CHOOSER_TARGETS_PER_APP; - float lastScore = 0; - boolean shouldNotify = false; - for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) { - final ChooserTarget target = targets.get(i); - float targetScore = target.getScore(); - targetScore *= baseScore; - if (i > 0 && targetScore >= lastScore) { - // Apply a decay so that the top app can't crowd out everything else. - // This incents ChooserTargetServices to define what's truly better. - targetScore = lastScore * 0.95f; - } - boolean isInserted = insertServiceTarget( - new SelectableTargetInfo(origTarget, target, targetScore)); - - if (isInserted && isShortcutResult) { - mNumShortcutResults++; - } - - shouldNotify |= isInserted; - - if (DEBUG) { - Log.d(TAG, " => " + target.toString() + " score=" + targetScore - + " base=" + target.getScore() - + " lastScore=" + lastScore - + " baseScore=" + baseScore); - } - - lastScore = targetScore; - } - - if (shouldNotify) { - notifyDataSetChanged(); - } - } - - private int getNumShortcutResults() { - return mNumShortcutResults; - } - - /** - * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: - *
    - *
  1. App-supplied targets - *
  2. Shortcuts ranked via App Prediction Manager - *
  3. Shortcuts ranked via legacy heuristics - *
  4. Legacy direct share targets - *
- */ - public float getBaseScore(DisplayResolveInfo target, @ShareTargetType int targetType) { - if (target == null) { - return CALLER_TARGET_SCORE_BOOST; - } - - if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { - return SHORTCUT_TARGET_SCORE_BOOST; - } - - float score = super.getScore(target); - if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) { - return score * SHORTCUT_TARGET_SCORE_BOOST; - } - - return score; - } - - /** - * Calling this marks service target loading complete, and will attempt to no longer - * update the direct share area. - */ - public void completeServiceTargetLoading() { - mServiceTargets.removeIf(o -> o instanceof PlaceHolderTargetInfo); - - if (mServiceTargets.isEmpty()) { - mServiceTargets.add(new EmptyTargetInfo()); - } - notifyDataSetChanged(); - } - - private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { - // Avoid inserting any potentially late results - if (mServiceTargets.size() == 1 - && mServiceTargets.get(0) instanceof EmptyTargetInfo) { - return false; - } - - // Check for duplicates and abort if found - for (ChooserTargetInfo otherTargetInfo : mServiceTargets) { - if (chooserTargetInfo.isSimilar(otherTargetInfo)) { - return false; - } - } - - int currentSize = mServiceTargets.size(); - final float newScore = chooserTargetInfo.getModifiedScore(); - for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) { - final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); - if (serviceTarget == null) { - mServiceTargets.set(i, chooserTargetInfo); - return true; - } else if (newScore > serviceTarget.getModifiedScore()) { - mServiceTargets.add(i, chooserTargetInfo); - return true; - } - } - - if (currentSize < MAX_SERVICE_TARGETS) { - mServiceTargets.add(chooserTargetInfo); - return true; - } - - return false; - } - } - static class BaseChooserTargetComparator implements Comparator { @Override public int compare(ChooserTarget lhs, ChooserTarget rhs) { @@ -2924,8 +2088,77 @@ public class ChooserActivity extends ResolverActivity { } } + @Override // ResolverListCommunicator + public void onHandlePackagesChanged() { + mServicesRequested.clear(); + mAdapter.notifyDataSetChanged(); + super.onHandlePackagesChanged(); + } - private boolean isSendAction(Intent targetIntent) { + @Override // SelectableTargetInfoCommunicator + public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) { + return mChooserListAdapter.makePresentationGetter(info); + } + + @Override // SelectableTargetInfoCommunicator + public Intent getReferrerFillInIntent() { + return mReferrerFillInIntent; + } + + @Override // ChooserListCommunicator + public int getMaxRankedTargets() { + return mChooserRowAdapter == null ? 4 : mChooserRowAdapter.getMaxTargetsPerRow(); + } + + @Override // ChooserListCommunicator + public void sendListViewUpdateMessage() { + mChooserHandler.sendEmptyMessageDelayed(ChooserHandler.LIST_VIEW_UPDATE_MESSAGE, + LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS); + } + + @Override + public void onListRebuilt() { + if (mChooserListAdapter.mDisplayList == null + || mChooserListAdapter.mDisplayList.isEmpty()) { + mChooserListAdapter.notifyDataSetChanged(); + } else { + new AsyncTask() { + @Override + protected Void doInBackground(Void... voids) { + mChooserListAdapter.updateAlphabeticalList(); + return null; + } + @Override + protected void onPostExecute(Void aVoid) { + mChooserListAdapter.notifyDataSetChanged(); + } + }.execute(); + } + + // don't support direct share on low ram devices + if (ActivityManager.isLowRamDeviceStatic()) { + return; + } + + if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS + || ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) { + if (DEBUG) { + Log.d(TAG, "querying direct share targets from ShortcutManager"); + } + + queryDirectShareTargets(mChooserListAdapter, false); + } + if (USE_CHOOSER_TARGET_SERVICE_FOR_DIRECT_TARGETS) { + if (DEBUG) { + Log.d(TAG, "List built querying services"); + } + + queryTargetServices(mChooserListAdapter); + } + } + + @Override // ChooserListCommunicator + public boolean isSendAction(Intent targetIntent) { if (targetIntent == null) { return false; } @@ -3080,7 +2313,8 @@ public class ChooserActivity extends ResolverActivity { // There can be at most one row in the listview, that is internally // a ViewGroup with 2 rows public int getServiceTargetRowCount() { - if (isSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) { + if (isSendAction(getTargetIntent()) + && !ActivityManager.isLowRamDeviceStatic()) { return 1; } return 0; @@ -3177,7 +2411,7 @@ public class ChooserActivity extends ResolverActivity { getResources().getDrawable(R.drawable.chooser_row_layer_list, null)); mProfileView = profileRow.findViewById(R.id.profile_button); mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); - bindProfileView(); + updateProfileViewButton(); return profileRow; } diff --git a/core/java/com/android/internal/app/ChooserFlags.java b/core/java/com/android/internal/app/ChooserFlags.java new file mode 100644 index 0000000000000..f1f1dbf49b8b1 --- /dev/null +++ b/core/java/com/android/internal/app/ChooserFlags.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019 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.app.prediction.AppPredictionManager; + +/** + * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}. + */ +public class ChooserFlags { + /** + * If set to true, use ShortcutManager to retrieve the matching direct share targets, instead of + * binding to every ChooserTargetService implementation. + */ + // TODO(b/121287573): Replace with a system flag (setprop?) + public static final boolean USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS = true; + + /** + * If {@link ChooserFlags#USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS} and this is set to true, + * {@link AppPredictionManager} will be queried for direct share targets. + */ + // TODO(b/123089490): Replace with system flag + static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true; +} diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java new file mode 100644 index 0000000000000..6eb470fef2bc8 --- /dev/null +++ b/core/java/com/android/internal/app/ChooserListAdapter.java @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2019 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 static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; +import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; + +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.LabeledIntent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.AsyncTask; +import android.os.UserManager; +import android.service.chooser.ChooserTarget; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import com.android.internal.R; +import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import com.android.internal.app.chooser.ChooserTargetInfo; +import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.SelectableTargetInfo; +import com.android.internal.app.chooser.TargetInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ChooserListAdapter extends ResolverListAdapter { + private static final String TAG = "ChooserListAdapter"; + private static final boolean DEBUG = false; + + public static final int TARGET_BAD = -1; + public static final int TARGET_CALLER = 0; + public static final int TARGET_SERVICE = 1; + public static final int TARGET_STANDARD = 2; + public static final int TARGET_STANDARD_AZ = 3; + + private static final int MAX_SUGGESTED_APP_TARGETS = 4; + private static final int MAX_CHOOSER_TARGETS_PER_APP = 2; + + static final int MAX_SERVICE_TARGETS = 8; + + /** {@link #getBaseScore} */ + public static final float CALLER_TARGET_SCORE_BOOST = 900.f; + /** {@link #getBaseScore} */ + public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f; + + private final int mMaxShortcutTargetsPerApp; + private final ChooserListCommunicator mChooserListCommunicator; + private final SelectableTargetInfo.SelectableTargetInfoCommunicator + mSelectableTargetInfoComunicator; + + private int mNumShortcutResults = 0; + + // Reserve spots for incoming direct share targets by adding placeholders + private ChooserTargetInfo + mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo(); + private final List mServiceTargets = new ArrayList<>(); + private final List mCallerTargets = new ArrayList<>(); + + private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator = + new ChooserActivity.BaseChooserTargetComparator(); + private boolean mListViewDataChanged = false; + + // Sorted list of DisplayResolveInfos for the alphabetical app section. + private List mSortedList = new ArrayList<>(); + + public ChooserListAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, + boolean filterLastUsed, ResolverListController resolverListController, + boolean useLayoutForBrowsables, + ChooserListCommunicator chooserListCommunicator, + SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoComunicator) { + // 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, filterLastUsed, + resolverListController, useLayoutForBrowsables, + chooserListCommunicator); + + createPlaceHolders(); + mMaxShortcutTargetsPerApp = + context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp); + mChooserListCommunicator = chooserListCommunicator; + mSelectableTargetInfoComunicator = selectableTargetInfoComunicator; + + if (initialIntents != null) { + final PackageManager pm = context.getPackageManager(); + for (int i = 0; i < initialIntents.length; i++) { + final Intent ii = initialIntents[i]; + if (ii == null) { + continue; + } + + // We reimplement Intent#resolveActivityInfo here because if we have an + // implicit intent, we want the ResolveInfo returned by PackageManager + // instead of one we reconstruct ourselves. The ResolveInfo returned might + // have extra metadata and resolvePackageName set and we want to respect that. + ResolveInfo ri = null; + ActivityInfo ai = null; + final ComponentName cn = ii.getComponent(); + if (cn != null) { + try { + ai = pm.getActivityInfo(ii.getComponent(), 0); + ri = new ResolveInfo(); + ri.activityInfo = ai; + } catch (PackageManager.NameNotFoundException ignored) { + // ai will == null below + } + } + if (ai == null) { + ri = pm.resolveActivity(ii, PackageManager.MATCH_DEFAULT_ONLY); + ai = ri != null ? ri.activityInfo : null; + } + if (ai == null) { + Log.w(TAG, "No activity found for " + ii); + continue; + } + UserManager userManager = + (UserManager) context.getSystemService(Context.USER_SERVICE); + if (ii instanceof LabeledIntent) { + LabeledIntent li = (LabeledIntent) ii; + ri.resolvePackageName = li.getSourcePackage(); + ri.labelRes = li.getLabelResource(); + ri.nonLocalizedLabel = li.getNonLocalizedLabel(); + ri.icon = li.getIconResource(); + ri.iconResourceId = ri.icon; + } + if (userManager.isManagedProfile()) { + ri.noResourceId = true; + ri.icon = 0; + } + mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri))); + } + } + } + + @Override + public void handlePackagesChanged() { + if (DEBUG) { + Log.d(TAG, "clearing queryTargets on package change"); + } + createPlaceHolders(); + mChooserListCommunicator.onHandlePackagesChanged(); + + } + + @Override + public void notifyDataSetChanged() { + if (!mListViewDataChanged) { + mChooserListCommunicator.sendListViewUpdateMessage(); + mListViewDataChanged = true; + } + } + + void refreshListView() { + if (mListViewDataChanged) { + super.notifyDataSetChanged(); + } + mListViewDataChanged = false; + } + + + private void createPlaceHolders() { + mNumShortcutResults = 0; + mServiceTargets.clear(); + for (int i = 0; i < MAX_SERVICE_TARGETS; i++) { + mServiceTargets.add(mPlaceHolderTargetInfo); + } + } + + @Override + public View onCreateView(ViewGroup parent) { + return mInflater.inflate( + com.android.internal.R.layout.resolve_grid_item, parent, false); + } + + @Override + protected void onBindView(View view, TargetInfo info) { + super.onBindView(view, info); + + // If target is loading, show a special placeholder shape in the label, make unclickable + final ViewHolder holder = (ViewHolder) view.getTag(); + if (info instanceof ChooserActivity.PlaceHolderTargetInfo) { + final int maxWidth = mContext.getResources().getDimensionPixelSize( + R.dimen.chooser_direct_share_label_placeholder_max_width); + holder.text.setMaxWidth(maxWidth); + holder.text.setBackground(mContext.getResources().getDrawable( + R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme())); + // Prevent rippling by removing background containing ripple + holder.itemView.setBackground(null); + } else { + holder.text.setMaxWidth(Integer.MAX_VALUE); + holder.text.setBackground(null); + holder.itemView.setBackground(holder.defaultItemViewBackground); + } + } + + void updateAlphabeticalList() { + mSortedList.clear(); + mSortedList.addAll(mDisplayList); + Collections.sort(mSortedList, new ChooserActivity.AzInfoComparator(mContext)); + } + + @Override + public boolean shouldGetResolvedFilter() { + return true; + } + + @Override + public int getCount() { + return getRankedTargetCount() + getAlphaTargetCount() + + getSelectableServiceTargetCount() + getCallerTargetCount(); + } + + @Override + public int getUnfilteredCount() { + int appTargets = super.getUnfilteredCount(); + if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) { + appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets(); + } + return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount(); + } + + + public int getCallerTargetCount() { + return Math.min(mCallerTargets.size(), MAX_SUGGESTED_APP_TARGETS); + } + + /** + * Filter out placeholders and non-selectable service targets + */ + public int getSelectableServiceTargetCount() { + int count = 0; + for (ChooserTargetInfo info : mServiceTargets) { + if (info instanceof SelectableTargetInfo) { + count++; + } + } + return count; + } + + public int getServiceTargetCount() { + if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent()) + && !ActivityManager.isLowRamDeviceStatic()) { + return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); + } + + return 0; + } + + int getAlphaTargetCount() { + int standardCount = super.getCount(); + return standardCount > mChooserListCommunicator.getMaxRankedTargets() ? standardCount : 0; + } + + int getRankedTargetCount() { + int spacesAvailable = + mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount(); + return Math.min(spacesAvailable, super.getCount()); + } + + public int getPositionTargetType(int position) { + int offset = 0; + + final int serviceTargetCount = getServiceTargetCount(); + if (position < serviceTargetCount) { + return TARGET_SERVICE; + } + offset += serviceTargetCount; + + final int callerTargetCount = getCallerTargetCount(); + if (position - offset < callerTargetCount) { + return TARGET_CALLER; + } + offset += callerTargetCount; + + final int rankedTargetCount = getRankedTargetCount(); + if (position - offset < rankedTargetCount) { + return TARGET_STANDARD; + } + offset += rankedTargetCount; + + final int standardTargetCount = getAlphaTargetCount(); + if (position - offset < standardTargetCount) { + return TARGET_STANDARD_AZ; + } + + return TARGET_BAD; + } + + @Override + public TargetInfo getItem(int position) { + return targetInfoForPosition(position, true); + } + + + /** + * Find target info for a given position. + * Since ChooserActivity displays several sections of content, determine which + * section provides this item. + */ + @Override + public TargetInfo targetInfoForPosition(int position, boolean filtered) { + int offset = 0; + + // Direct share targets + final int serviceTargetCount = filtered ? getServiceTargetCount() : + getSelectableServiceTargetCount(); + if (position < serviceTargetCount) { + return mServiceTargets.get(position); + } + offset += serviceTargetCount; + + // Targets provided by calling app + final int callerTargetCount = getCallerTargetCount(); + if (position - offset < callerTargetCount) { + return mCallerTargets.get(position - offset); + } + offset += callerTargetCount; + + // Ranked standard app targets + final int rankedTargetCount = getRankedTargetCount(); + if (position - offset < rankedTargetCount) { + return filtered ? super.getItem(position - offset) + : getDisplayResolveInfo(position - offset); + } + offset += rankedTargetCount; + + // Alphabetical complete app target list. + if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) { + return mSortedList.get(position - offset); + } + + return null; + } + + + /** + * Evaluate targets for inclusion in the direct share area. May not be included + * if score is too low. + */ + public void addServiceResults(DisplayResolveInfo origTarget, List targets, + @ChooserActivity.ShareTargetType int targetType) { + if (DEBUG) { + Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() + + " targets"); + } + + if (targets.size() == 0) { + return; + } + + final float baseScore = getBaseScore(origTarget, targetType); + Collections.sort(targets, mBaseTargetComparator); + + final boolean isShortcutResult = + (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER + || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); + final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp + : MAX_CHOOSER_TARGETS_PER_APP; + float lastScore = 0; + boolean shouldNotify = false; + for (int i = 0, count = Math.min(targets.size(), maxTargets); i < count; i++) { + final ChooserTarget target = targets.get(i); + float targetScore = target.getScore(); + targetScore *= baseScore; + if (i > 0 && targetScore >= lastScore) { + // Apply a decay so that the top app can't crowd out everything else. + // This incents ChooserTargetServices to define what's truly better. + targetScore = lastScore * 0.95f; + } + boolean isInserted = insertServiceTarget(new SelectableTargetInfo( + mContext, origTarget, target, targetScore, mSelectableTargetInfoComunicator)); + + if (isInserted && isShortcutResult) { + mNumShortcutResults++; + } + + shouldNotify |= isInserted; + + if (DEBUG) { + Log.d(TAG, " => " + target.toString() + " score=" + targetScore + + " base=" + target.getScore() + + " lastScore=" + lastScore + + " baseScore=" + baseScore); + } + + lastScore = targetScore; + } + + if (shouldNotify) { + notifyDataSetChanged(); + } + } + + int getNumShortcutResults() { + return mNumShortcutResults; + } + + /** + * Use the scoring system along with artificial boosts to create up to 4 distinct buckets: + *
    + *
  1. App-supplied targets + *
  2. Shortcuts ranked via App Prediction Manager + *
  3. Shortcuts ranked via legacy heuristics + *
  4. Legacy direct share targets + *
+ */ + public float getBaseScore( + DisplayResolveInfo target, + @ChooserActivity.ShareTargetType int targetType) { + if (target == null) { + return CALLER_TARGET_SCORE_BOOST; + } + + if (targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) { + return SHORTCUT_TARGET_SCORE_BOOST; + } + + float score = super.getScore(target); + if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) { + return score * SHORTCUT_TARGET_SCORE_BOOST; + } + + return score; + } + + /** + * Calling this marks service target loading complete, and will attempt to no longer + * update the direct share area. + */ + public void completeServiceTargetLoading() { + mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo); + + if (mServiceTargets.isEmpty()) { + mServiceTargets.add(new ChooserActivity.EmptyTargetInfo()); + } + notifyDataSetChanged(); + } + + private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { + // Avoid inserting any potentially late results + if (mServiceTargets.size() == 1 + && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) { + return false; + } + + // Check for duplicates and abort if found + for (ChooserTargetInfo otherTargetInfo : mServiceTargets) { + if (chooserTargetInfo.isSimilar(otherTargetInfo)) { + return false; + } + } + + int currentSize = mServiceTargets.size(); + final float newScore = chooserTargetInfo.getModifiedScore(); + for (int i = 0; i < Math.min(currentSize, MAX_SERVICE_TARGETS); i++) { + final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); + if (serviceTarget == null) { + mServiceTargets.set(i, chooserTargetInfo); + return true; + } else if (newScore > serviceTarget.getModifiedScore()) { + mServiceTargets.add(i, chooserTargetInfo); + return true; + } + } + + if (currentSize < MAX_SERVICE_TARGETS) { + mServiceTargets.add(chooserTargetInfo); + return true; + } + + return false; + } + + public ChooserTarget getChooserTargetForValue(int value) { + return mServiceTargets.get(value).getChooserTarget(); + } + + /** + * Rather than fully sorting the input list, this sorting task will put the top k elements + * in the head of input list and fill the tail with other elements in undetermined order. + */ + @Override + AsyncTask, + Void, + List> createSortingTask() { + return new AsyncTask, + Void, + List>() { + @Override + protected List doInBackground( + List... params) { + mResolverListController.topK(params[0], + mChooserListCommunicator.getMaxRankedTargets()); + return params[0]; + } + @Override + protected void onPostExecute(List sortedComponents) { + processSortedList(sortedComponents); + mChooserListCommunicator.updateProfileViewButton(); + notifyDataSetChanged(); + } + }; + } + + /** + * Necessary methods to communicate between {@link ChooserListAdapter} + * and {@link ChooserActivity}. + */ + interface ChooserListCommunicator extends ResolverListCommunicator { + + int getMaxRankedTargets(); + + void sendListViewUpdateMessage(); + + boolean isSendAction(Intent targetIntent); + } +} diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 74996e9fc2127..c1c6ac9dfa893 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -23,7 +23,6 @@ import android.annotation.StringRes; import android.annotation.UiThread; import android.annotation.UnsupportedAppUsage; import android.app.Activity; -import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.ActivityThread; import android.app.VoiceInteractor.PickOptionRequest; @@ -35,26 +34,18 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.LabeledIntent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.content.res.Configuration; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.ColorMatrix; -import android.graphics.ColorMatrixColorFilter; import android.graphics.Insets; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.PatternMatcher; -import android.os.Process; import android.os.RemoteException; import android.os.StrictMode; import android.os.UserHandle; @@ -71,7 +62,6 @@ import android.view.ViewGroup.LayoutParams; import android.view.WindowInsets; import android.widget.AbsListView; import android.widget.AdapterView; -import android.widget.BaseAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.ListView; @@ -81,6 +71,8 @@ import android.widget.Toast; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.TargetInfo; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; @@ -99,32 +91,33 @@ import java.util.Set; * which to go to. It is not normally used directly by application developers. */ @UiThread -public class ResolverActivity extends Activity { - - // Temporary flag for new chooser delegate behavior. - boolean mEnableChooserDelegate = true; +public class ResolverActivity extends Activity implements + ResolverListAdapter.ResolverListCommunicator { @UnsupportedAppUsage - protected ResolveListAdapter mAdapter; + protected ResolverListAdapter mAdapter; private boolean mSafeForwardingMode; protected AbsListView mAdapterView; private Button mAlwaysButton; private Button mOnceButton; protected View mProfileView; - private int mIconDpi; private int mLastSelected = AbsListView.INVALID_POSITION; private boolean mResolvingHome = false; private int mProfileSwitchMessageId = -1; private int mLayoutId; - private final ArrayList mIntents = new ArrayList<>(); + @VisibleForTesting + protected final ArrayList mIntents = new ArrayList<>(); private PickTargetOptionRequest mPickOptionRequest; private String mReferrerPackage; private CharSequence mTitle; private int mDefaultTitleResId; - private boolean mUseLayoutForBrowsables; + + @VisibleForTesting + protected boolean mUseLayoutForBrowsables; // Whether or not this activity supports choosing a default handler for the intent. - private boolean mSupportsAlwaysUseOption; + @VisibleForTesting + protected boolean mSupportsAlwaysUseOption; protected ResolverDrawerLayout mResolverDrawerLayout; @UnsupportedAppUsage protected PackageManager mPm; @@ -132,12 +125,9 @@ public class ResolverActivity extends Activity { private static final String TAG = "ResolverActivity"; private static final boolean DEBUG = false; - private Runnable mPostListReadyRunnable; private boolean mRegistered; - private ColorMatrixColorFilter mSuspendedMatrixColorFilter; - protected Insets mSystemWindowInsets = null; private Space mFooterSpacer = null; @@ -233,7 +223,7 @@ public class ResolverActivity extends Activity { @Override public void onSomePackagesChanged() { mAdapter.handlePackagesChanged(); - bindProfileView(); + updateProfileViewButton(); } @Override @@ -316,9 +306,6 @@ public class ResolverActivity extends Activity { mRegistered = true; mReferrerPackage = getReferrerPackageName(); - final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); - mIconDpi = am.getLauncherLargeIconDensity(); - // Add our initial intent as the first item, regardless of what else has already been added. mIntents.add(0, new Intent(intent)); mTitle = title; @@ -330,7 +317,17 @@ public class ResolverActivity extends Activity { mSupportsAlwaysUseOption = supportsAlwaysUseOption; - if (configureContentView(mIntents, initialIntents, rList)) { + // The last argument of createAdapter is whether to do special handling + // of the last used choice to highlight it in the list. We need to always + // turn this off when running under voice interaction, since it results in + // a more complicated UI that the current voice interaction flow is not able + // to handle. + boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction(); + mAdapter = createAdapter(this, mIntents, initialIntents, rList, + filterLastUsed, mUseLayoutForBrowsables); + configureContentView(); + + if (rebuildList()) { return; } @@ -356,11 +353,9 @@ public class ResolverActivity extends Activity { mProfileView = findViewById(R.id.profile_button); if (mProfileView != null) { mProfileView.setOnClickListener(this::onProfileClick); - bindProfileView(); + updateProfileViewButton(); } - initSuspendedColorMatrix(); - final Set categories = intent.getCategories(); MetricsLogger.action(this, mAdapter.hasFilteredItem() ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED @@ -423,25 +418,7 @@ public class ResolverActivity extends Activity { } } - private void initSuspendedColorMatrix() { - int grayValue = 127; - float scale = 0.5f; // half bright - - ColorMatrix tempBrightnessMatrix = new ColorMatrix(); - float[] mat = tempBrightnessMatrix.getArray(); - mat[0] = scale; - mat[6] = scale; - mat[12] = scale; - mat[4] = grayValue; - mat[9] = grayValue; - mat[14] = grayValue; - - ColorMatrix matrix = new ColorMatrix(); - matrix.setSaturation(0.0f); - matrix.preConcat(tempBrightnessMatrix); - mSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix); - } - + @Override // ResolverListCommunicator public void sendVoiceChoicesIfNeeded() { if (!isVoiceInteraction()) { // Clearly not needed. @@ -476,6 +453,7 @@ public class ResolverActivity extends Activity { } } + @Override // SelectableTargetInfoCommunicator ResolverListCommunicator public Intent getTargetIntent() { return mIntents.isEmpty() ? null : mIntents.get(0); } @@ -492,7 +470,8 @@ public class ResolverActivity extends Activity { return R.layout.resolver_list; } - protected void bindProfileView() { + @Override // ResolverListCommunicator + public void updateProfileViewButton() { if (mProfileView == null) { return; } @@ -583,187 +562,6 @@ public class ResolverActivity extends Activity { } } - - /** - * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application - * icon and label over any IntentFilter or Activity icon to increase user understanding, with an - * exception for applications that hold the right permission. Always attempts to use available - * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses - * Strings to strip creative formatting. - */ - private abstract static class TargetPresentationGetter { - @Nullable abstract Drawable getIconSubstituteInternal(); - @Nullable abstract String getAppSubLabelInternal(); - - private Context mCtx; - private final int mIconDpi; - private final boolean mHasSubstitutePermission; - private final ApplicationInfo mAi; - - protected PackageManager mPm; - - TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { - mCtx = ctx; - mPm = ctx.getPackageManager(); - mAi = ai; - mIconDpi = iconDpi; - mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( - android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, - mAi.packageName); - } - - public Drawable getIcon(UserHandle userHandle) { - return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); - } - - public Bitmap getIconBitmap(UserHandle userHandle) { - Drawable dr = null; - if (mHasSubstitutePermission) { - dr = getIconSubstituteInternal(); - } - - if (dr == null) { - try { - if (mAi.icon != 0) { - dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); - } - } catch (NameNotFoundException ignore) { - } - } - - // Fall back to ApplicationInfo#loadIcon if nothing has been loaded - if (dr == null) { - dr = mAi.loadIcon(mPm); - } - - SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); - Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); - sif.recycle(); - - return icon; - } - - public String getLabel() { - String label = null; - // Apps with the substitute permission will always show the sublabel as their label - if (mHasSubstitutePermission) { - label = getAppSubLabelInternal(); - } - - if (label == null) { - label = (String) mAi.loadLabel(mPm); - } - - return label; - } - - public String getSubLabel() { - // Apps with the substitute permission will never have a sublabel - if (mHasSubstitutePermission) return null; - return getAppSubLabelInternal(); - } - - protected String loadLabelFromResource(Resources res, int resId) { - return res.getString(resId); - } - - @Nullable - protected Drawable loadIconFromResource(Resources res, int resId) { - return res.getDrawableForDensity(resId, mIconDpi); - } - - } - - /** - * Loads the icon and label for the provided ResolveInfo. - */ - @VisibleForTesting - public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { - private final ResolveInfo mRi; - public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { - super(ctx, iconDpi, ri.activityInfo); - mRi = ri; - } - - @Override - Drawable getIconSubstituteInternal() { - Drawable dr = null; - try { - // Do not use ResolveInfo#getIconResource() as it defaults to the app - if (mRi.resolvePackageName != null && mRi.icon != 0) { - dr = loadIconFromResource( - mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); - } - } catch (NameNotFoundException e) { - Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " - + "couldn't find resources for package", e); - } - - // Fall back to ActivityInfo if no icon is found via ResolveInfo - if (dr == null) dr = super.getIconSubstituteInternal(); - - return dr; - } - - @Override - String getAppSubLabelInternal() { - // Will default to app name if no intent filter or activity label set, make sure to - // check if subLabel matches label before final display - return (String) mRi.loadLabel(mPm); - } - } - - ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { - return new ResolveInfoPresentationGetter(this, mIconDpi, ri); - } - - /** - * Loads the icon and label for the provided ActivityInfo. - */ - @VisibleForTesting - public static class ActivityInfoPresentationGetter extends TargetPresentationGetter { - private final ActivityInfo mActivityInfo; - public ActivityInfoPresentationGetter(Context ctx, int iconDpi, - ActivityInfo activityInfo) { - super(ctx, iconDpi, activityInfo.applicationInfo); - mActivityInfo = activityInfo; - } - - @Override - Drawable getIconSubstituteInternal() { - Drawable dr = null; - try { - // Do not use ActivityInfo#getIconResource() as it defaults to the app - if (mActivityInfo.icon != 0) { - dr = loadIconFromResource( - mPm.getResourcesForApplication(mActivityInfo.applicationInfo), - mActivityInfo.icon); - } - } catch (NameNotFoundException e) { - Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " - + "couldn't find resources for package", e); - } - - return dr; - } - - @Override - String getAppSubLabelInternal() { - // Will default to app name if no activity label set, make sure to check if subLabel - // matches label before final display - return (String) mActivityInfo.loadLabel(mPm); - } - } - - protected ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { - return new ActivityInfoPresentationGetter(this, mIconDpi, ai); - } - - Drawable loadIconForResolveInfo(ResolveInfo ri) { - // Load icons based on the current process. If in work profile icons should be badged. - return makePresentationGetter(ri).getIcon(Process.myUserHandle()); - } - @Override protected void onRestart() { super.onRestart(); @@ -772,7 +570,7 @@ public class ResolverActivity extends Activity { mRegistered = true; } mAdapter.handlePackagesChanged(); - bindProfileView(); + updateProfileViewButton(); } @Override @@ -804,12 +602,8 @@ public class ResolverActivity extends Activity { if (!isChangingConfigurations() && mPickOptionRequest != null) { mPickOptionRequest.cancel(); } - if (mPostListReadyRunnable != null) { - getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); - mPostListReadyRunnable = null; - } - if (mAdapter != null && mAdapter.mResolverListController != null) { - mAdapter.mResolverListController.destroy(); + if (mAdapter != null) { + mAdapter.onDestroy(); } } @@ -950,10 +744,30 @@ public class ResolverActivity extends Activity { /** * Replace me in subclasses! */ + @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { return defIntent; } + @Override // ResolverListCommunicator + public void onPostListReady() { + setHeader(); + resetButtonBar(); + onListRebuilt(); + } + + protected void onListRebuilt() { + 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); + finish(); + } + } + } + protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { final ResolveInfo ri = target.getResolveInfo(); final Intent intent = target != null ? target.getResolvedIntent() : null; @@ -1049,8 +863,8 @@ public class ResolverActivity extends Activity { // If we don't add back in the component for forwarding the intent to a managed // profile, the preferred activity may not be updated correctly (as the set of // components we tell it we knew about will have changed). - final boolean needToAddBackProfileForwardingComponent - = mAdapter.mOtherProfile != null; + final boolean needToAddBackProfileForwardingComponent = + mAdapter.getOtherProfile() != null; if (!needToAddBackProfileForwardingComponent) { set = new ComponentName[N]; } else { @@ -1066,8 +880,8 @@ public class ResolverActivity extends Activity { } if (needToAddBackProfileForwardingComponent) { - set[N] = mAdapter.mOtherProfile.getResolvedComponentName(); - final int otherProfileMatch = mAdapter.mOtherProfile.getResolveInfo().match; + set[N] = mAdapter.getOtherProfile().getResolvedComponentName(); + final int otherProfileMatch = mAdapter.getOtherProfile().getResolveInfo().match; if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch; } @@ -1169,7 +983,7 @@ public class ResolverActivity extends Activity { } - boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity, + public boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity, int userId) { // Pass intent to delegate chooser activity with permission token. // TODO: This should move to a trampoline Activity in the system when the ChooserActivity @@ -1205,6 +1019,7 @@ public class ResolverActivity extends Activity { // Do nothing } + @Override // ResolverListCommunicator public boolean shouldGetActivityMetadata() { return false; } @@ -1220,11 +1035,11 @@ public class ResolverActivity extends Activity { startActivity(in); } - public ResolveListAdapter createAdapter(Context context, List payloadIntents, - Intent[] initialIntents, List rList, int launchedFromUid, - boolean filterLastUsed) { - return new ResolveListAdapter(context, payloadIntents, initialIntents, rList, - launchedFromUid, filterLastUsed, createListController()); + public ResolverListAdapter createAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, + boolean filterLastUsed, boolean useLayoutForBrowsables) { + return new ResolverListAdapter(context, payloadIntents, initialIntents, rList, + filterLastUsed, createListController(), useLayoutForBrowsables, this); } @VisibleForTesting @@ -1238,26 +1053,34 @@ public class ResolverActivity extends Activity { } /** - * Returns true if the activity is finishing and creation should halt + * Sets up the content view. */ - public boolean configureContentView(List payloadIntents, Intent[] initialIntents, - List rList) { - // The last argument of createAdapter is whether to do special handling - // of the last used choice to highlight it in the list. We need to always - // turn this off when running under voice interaction, since it results in - // a more complicated UI that the current voice interaction flow is not able - // to handle. - mAdapter = createAdapter(this, payloadIntents, initialIntents, rList, - mLaunchedFromUid, mSupportsAlwaysUseOption && !isVoiceInteraction()); - boolean rebuildCompleted = mAdapter.rebuildList(); - + private void configureContentView() { + if (mAdapter == null) { + throw new IllegalStateException("mAdapter cannot be null."); + } if (useLayoutWithDefault()) { mLayoutId = R.layout.resolver_list_with_default; } else { mLayoutId = getLayoutResource(); } setContentView(mLayoutId); + mAdapterView = findViewById(R.id.resolver_list); + } + /** + * Returns true if the activity is finishing and creation should halt. + *

Subclasses must call rebuildListInternal at the end of rebuildList. + */ + protected boolean rebuildList() { + return rebuildListInternal(); + } + + /** + * Returns true if the activity is finishing and creation should halt. + */ + final boolean rebuildListInternal() { + boolean rebuildCompleted = mAdapter.rebuildList(); int count = mAdapter.getUnfilteredCount(); // We only rebuild asynchronously when we have multiple elements to sort. In the case where @@ -1276,10 +1099,7 @@ public class ResolverActivity extends Activity { } } - - mAdapterView = findViewById(R.id.resolver_list); - - if (count == 0 && mAdapter.mPlaceholderCount == 0) { + if (count == 0 && mAdapter.getPlaceholderCount() == 0) { final TextView emptyView = findViewById(R.id.empty); emptyView.setVisibility(View.VISIBLE); mAdapterView.setVisibility(View.GONE); @@ -1290,7 +1110,7 @@ public class ResolverActivity extends Activity { return false; } - public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter) { + public void onPrepareAdapterView(AbsListView adapterView, ResolverListAdapter adapter) { final boolean useHeader = adapter.hasFilteredItem(); final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; @@ -1317,7 +1137,7 @@ public class ResolverActivity extends Activity { * Configure the area above the app selection list (title, content preview, etc). */ public void setHeader() { - if (mAdapter.getCount() == 0 && mAdapter.mPlaceholderCount == 0) { + if (mAdapter.getCount() == 0 && mAdapter.getPlaceholderCount() == 0) { final TextView titleView = findViewById(R.id.title); if (titleView != null) { titleView.setVisibility(View.GONE); @@ -1337,9 +1157,8 @@ public class ResolverActivity extends Activity { } final ImageView iconView = findViewById(R.id.icon); - final DisplayResolveInfo iconInfo = mAdapter.getFilteredItem(); - if (iconView != null && iconInfo != null) { - new LoadIconTask(iconInfo, iconView).execute(); + if (iconView != null) { + mAdapter.loadFilteredItemIconTaskAsync(iconView); } } @@ -1382,7 +1201,8 @@ public class ResolverActivity extends Activity { } } - private boolean useLayoutWithDefault() { + @Override // ResolverListCommunicator + public boolean useLayoutWithDefault() { return mSupportsAlwaysUseOption && mAdapter.hasFilteredItem(); } @@ -1397,704 +1217,20 @@ public class ResolverActivity extends Activity { /** * Check a simple match for the component of two ResolveInfos. */ - static boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) { + @Override // ResolverListCommunicator + public boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) { return lhs == null ? rhs == null : lhs.activityInfo == null ? rhs.activityInfo == null : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name) && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName); } - public final class DisplayResolveInfo implements TargetInfo { - private final ResolveInfo mResolveInfo; - private CharSequence mDisplayLabel; - private Drawable mDisplayIcon; - private Drawable mBadge; - private CharSequence mExtendedInfo; - private final Intent mResolvedIntent; - private final List mSourceIntents = new ArrayList<>(); - private boolean mIsSuspended; - - public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent) { - this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent); + @Override // ResolverListCommunicator + public void onHandlePackagesChanged() { + if (mAdapter.getCount() == 0) { + // We no longer have any items... just finish the activity. + finish(); } - - public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, - CharSequence pInfo, Intent pOrigIntent) { - mSourceIntents.add(originalIntent); - mResolveInfo = pri; - mDisplayLabel = pLabel; - mExtendedInfo = pInfo; - - final Intent intent = new Intent(pOrigIntent != null ? pOrigIntent : - getReplacementIntent(pri.activityInfo, getTargetIntent())); - intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT - | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); - final ActivityInfo ai = mResolveInfo.activityInfo; - intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); - - mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; - - mResolvedIntent = intent; - } - - private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags) { - mSourceIntents.addAll(other.getAllSourceIntents()); - mResolveInfo = other.mResolveInfo; - mDisplayLabel = other.mDisplayLabel; - mDisplayIcon = other.mDisplayIcon; - mExtendedInfo = other.mExtendedInfo; - mResolvedIntent = new Intent(other.mResolvedIntent); - mResolvedIntent.fillIn(fillInIntent, flags); - } - - public ResolveInfo getResolveInfo() { - return mResolveInfo; - } - - public CharSequence getDisplayLabel() { - if (mDisplayLabel == null) { - ResolveInfoPresentationGetter pg = makePresentationGetter(mResolveInfo); - mDisplayLabel = pg.getLabel(); - mExtendedInfo = pg.getSubLabel(); - } - return mDisplayLabel; - } - - public boolean hasDisplayLabel() { - return mDisplayLabel != null; - } - - public void setDisplayLabel(CharSequence displayLabel) { - mDisplayLabel = displayLabel; - } - - public void setExtendedInfo(CharSequence extendedInfo) { - mExtendedInfo = extendedInfo; - } - - public Drawable getDisplayIcon() { - return mDisplayIcon; - } - - @Override - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new DisplayResolveInfo(this, fillInIntent, flags); - } - - @Override - public List getAllSourceIntents() { - return mSourceIntents; - } - - public void addAlternateSourceIntent(Intent alt) { - mSourceIntents.add(alt); - } - - public void setDisplayIcon(Drawable icon) { - mDisplayIcon = icon; - } - - public boolean hasDisplayIcon() { - return mDisplayIcon != null; - } - - public CharSequence getExtendedInfo() { - return mExtendedInfo; - } - - public Intent getResolvedIntent() { - return mResolvedIntent; - } - - @Override - public ComponentName getResolvedComponentName() { - return new ComponentName(mResolveInfo.activityInfo.packageName, - mResolveInfo.activityInfo.name); - } - - @Override - public boolean start(Activity activity, Bundle options) { - activity.startActivity(mResolvedIntent, options); - return true; - } - - @Override - public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { - if (mEnableChooserDelegate) { - return activity.startAsCallerImpl(mResolvedIntent, options, false, userId); - } else { - activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId); - return true; - } - } - - @Override - public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - activity.startActivityAsUser(mResolvedIntent, options, user); - return false; - } - - public boolean isSuspended() { - return mIsSuspended; - } - } - - List getDisplayList() { - return mAdapter.mDisplayList; - } - - /** - * A single target as represented in the chooser. - */ - public interface TargetInfo { - /** - * Get the resolved intent that represents this target. Note that this may not be the - * intent that will be launched by calling one of the start methods provided; - * this is the intent that will be credited with the launch. - * - * @return the resolved intent for this target - */ - Intent getResolvedIntent(); - - /** - * Get the resolved component name that represents this target. Note that this may not - * be the component that will be directly launched by calling one of the start - * methods provided; this is the component that will be credited with the launch. - * - * @return the resolved ComponentName for this target - */ - ComponentName getResolvedComponentName(); - - /** - * Start the activity referenced by this target. - * - * @param activity calling Activity performing the launch - * @param options ActivityOptions bundle - * @return true if the start completed successfully - */ - boolean start(Activity activity, Bundle options); - - /** - * Start the activity referenced by this target as if the ResolverActivity's caller - * was performing the start operation. - * - * @param activity calling Activity (actually) performing the launch - * @param options ActivityOptions bundle - * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller - * @return true if the start completed successfully - */ - boolean startAsCaller(ResolverActivity activity, Bundle options, int userId); - - /** - * Start the activity referenced by this target as a given user. - * - * @param activity calling activity performing the launch - * @param options ActivityOptions bundle - * @param user handle for the user to start the activity as - * @return true if the start completed successfully - */ - boolean startAsUser(Activity activity, Bundle options, UserHandle user); - - /** - * Return the ResolveInfo about how and why this target matched the original query - * for available targets. - * - * @return ResolveInfo representing this target's match - */ - ResolveInfo getResolveInfo(); - - /** - * Return the human-readable text label for this target. - * - * @return user-visible target label - */ - CharSequence getDisplayLabel(); - - /** - * Return any extended info for this target. This may be used to disambiguate - * otherwise identical targets. - * - * @return human-readable disambig string or null if none present - */ - CharSequence getExtendedInfo(); - - /** - * @return The drawable that should be used to represent this target including badge - */ - Drawable getDisplayIcon(); - - /** - * Clone this target with the given fill-in information. - */ - TargetInfo cloneFilledIn(Intent fillInIntent, int flags); - - /** - * @return the list of supported source intents deduped against this single target - */ - List getAllSourceIntents(); - - /** - * @return true if this target can be selected by the user - */ - boolean isSuspended(); - } - - public class ResolveListAdapter extends BaseAdapter { - private final List mIntents; - private final Intent[] mInitialIntents; - private final List mBaseResolveList; - protected ResolveInfo mLastChosen; - private DisplayResolveInfo mOtherProfile; - ResolverListController mResolverListController; - private int mPlaceholderCount; - private boolean mAllTargetsAreBrowsers = false; - - protected final LayoutInflater mInflater; - - // This one is the list that the Adapter will actually present. - List mDisplayList; - List mUnfilteredResolveList; - - private int mLastChosenPosition = -1; - private boolean mFilterLastUsed; - - public ResolveListAdapter(Context context, List payloadIntents, - Intent[] initialIntents, List rList, int launchedFromUid, - boolean filterLastUsed, - ResolverListController resolverListController) { - mIntents = payloadIntents; - mInitialIntents = initialIntents; - mBaseResolveList = rList; - mLaunchedFromUid = launchedFromUid; - mInflater = LayoutInflater.from(context); - mDisplayList = new ArrayList<>(); - mFilterLastUsed = filterLastUsed; - mResolverListController = resolverListController; - } - - public void handlePackagesChanged() { - rebuildList(); - if (getCount() == 0) { - // We no longer have any items... just finish the activity. - finish(); - } - } - - public void setPlaceholderCount(int count) { - mPlaceholderCount = count; - } - - public int getPlaceholderCount() { return mPlaceholderCount; } - - @Nullable - public DisplayResolveInfo getFilteredItem() { - if (mFilterLastUsed && mLastChosenPosition >= 0) { - // Not using getItem since it offsets to dodge this position for the list - return mDisplayList.get(mLastChosenPosition); - } - return null; - } - - public DisplayResolveInfo getOtherProfile() { - return mOtherProfile; - } - - public int getFilteredPosition() { - if (mFilterLastUsed && mLastChosenPosition >= 0) { - return mLastChosenPosition; - } - return AbsListView.INVALID_POSITION; - } - - public boolean hasFilteredItem() { - return mFilterLastUsed && mLastChosen != null; - } - - public float getScore(DisplayResolveInfo target) { - return mResolverListController.getScore(target); - } - - public void updateModel(ComponentName componentName) { - mResolverListController.updateModel(componentName); - } - - public void updateChooserCounts(String packageName, int userId, String action) { - mResolverListController.updateChooserCounts(packageName, userId, action); - } - - /** - * @return true if all items in the display list are defined as browsers by - * ResolveInfo.handleAllWebDataURI - */ - public boolean areAllTargetsBrowsers() { - return mAllTargetsAreBrowsers; - } - - /** - * 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; - // Clear the value of mOtherProfile from previous call. - mOtherProfile = null; - mLastChosen = null; - mLastChosenPosition = -1; - mAllTargetsAreBrowsers = false; - mDisplayList.clear(); - if (mBaseResolveList != null) { - currentResolveList = mUnfilteredResolveList = new ArrayList<>(); - mResolverListController.addResolveListDedupe(currentResolveList, - getTargetIntent(), - mBaseResolveList); - } else { - currentResolveList = mUnfilteredResolveList = - mResolverListController.getResolversForIntent(shouldGetResolvedFilter(), - shouldGetActivityMetadata(), - mIntents); - if (currentResolveList == null) { - processSortedList(currentResolveList); - return true; - } - List originalList = - mResolverListController.filterIneligibleActivities(currentResolveList, - true); - if (originalList != null) { - mUnfilteredResolveList = originalList; - } - } - - // So far we only support a single other profile at a time. - // The first one we see gets special treatment. - for (ResolvedComponentInfo info : currentResolveList) { - if (info.getResolveInfoAt(0).targetUserId != UserHandle.USER_CURRENT) { - mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), - info.getResolveInfoAt(0), - info.getResolveInfoAt(0).loadLabel(mPm), - info.getResolveInfoAt(0).loadLabel(mPm), - getReplacementIntent(info.getResolveInfoAt(0).activityInfo, - info.getIntentAt(0))); - currentResolveList.remove(info); - break; - } - } - - if (mOtherProfile == null) { - try { - mLastChosen = mResolverListController.getLastChosen(); - } catch (RemoteException re) { - Log.d(TAG, "Error calling getLastChosenActivity\n" + re); - } - } - - int N; - if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) { - // 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 (currentResolveList.size() > 1) { - int placeholderCount = currentResolveList.size(); - if (useLayoutWithDefault()) { - --placeholderCount; - } - setPlaceholderCount(placeholderCount); - createSortingTask().execute(currentResolveList); - postListReadyRunnable(); - return false; - } else { - processSortedList(currentResolveList); - return true; - } - } else { - processSortedList(currentResolveList); - return true; - } - } - - AsyncTask, - Void, - List> createSortingTask() { - return 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); - bindProfileView(); - notifyDataSetChanged(); - } - }; - } - - void processSortedList(List sortedComponents) { - int N; - if (sortedComponents != null && (N = sortedComponents.size()) != 0) { - mAllTargetsAreBrowsers = mUseLayoutForBrowsables; - - // First put the initial items at the top. - if (mInitialIntents != null) { - for (int i = 0; i < mInitialIntents.length; i++) { - Intent ii = mInitialIntents[i]; - if (ii == null) { - continue; - } - ActivityInfo ai = ii.resolveActivityInfo( - getPackageManager(), 0); - if (ai == null) { - Log.w(TAG, "No activity found for " + ii); - continue; - } - ResolveInfo ri = new ResolveInfo(); - ri.activityInfo = ai; - UserManager userManager = - (UserManager) getSystemService(Context.USER_SERVICE); - if (ii instanceof LabeledIntent) { - LabeledIntent li = (LabeledIntent) ii; - ri.resolvePackageName = li.getSourcePackage(); - ri.labelRes = li.getLabelResource(); - ri.nonLocalizedLabel = li.getNonLocalizedLabel(); - ri.icon = li.getIconResource(); - ri.iconResourceId = ri.icon; - } - if (userManager.isManagedProfile()) { - ri.noResourceId = true; - ri.icon = 0; - } - mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; - - addResolveInfo(new DisplayResolveInfo(ii, ri, - ri.loadLabel(getPackageManager()), null, ii)); - } - } - - - for (ResolvedComponentInfo rci : sortedComponents) { - final ResolveInfo ri = rci.getResolveInfoAt(0); - if (ri != null) { - mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; - addResolveInfoWithAlternates(rci); - } - } - } - - sendVoiceChoicesIfNeeded(); - postListReadyRunnable(); - } - - - - /** - * Some necessary methods for creating the list are initiated in onCreate and will also - * determine the layout known. We therefore can't update the UI inline and post to the - * handler thread to update after the current task is finished. - */ - private void postListReadyRunnable() { - if (mPostListReadyRunnable == null) { - mPostListReadyRunnable = new Runnable() { - @Override - public void run() { - setHeader(); - resetButtonBar(); - onListRebuilt(); - mPostListReadyRunnable = null; - } - }; - getMainThreadHandler().post(mPostListReadyRunnable); - } - } - - public void onListRebuilt() { - int count = getUnfilteredCount(); - if (count == 1 && getOtherProfile() == null) { - // Only one target, so we're a candidate to auto-launch! - final TargetInfo target = targetInfoForPosition(0, false); - if (shouldAutoLaunchSingleChoice(target)) { - safelyStartActivity(target); - finish(); - } - } - } - - public boolean shouldGetResolvedFilter() { - return mFilterLastUsed; - } - - private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { - final int count = rci.getCount(); - final Intent intent = rci.getIntentAt(0); - final ResolveInfo add = rci.getResolveInfoAt(0); - final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent); - final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, replaceIntent); - addResolveInfo(dri); - if (replaceIntent == intent) { - // Only add alternates if we didn't get a specific replacement from - // the caller. If we have one it trumps potential alternates. - for (int i = 1, N = count; i < N; i++) { - final Intent altIntent = rci.getIntentAt(i); - dri.addAlternateSourceIntent(altIntent); - } - } - updateLastChosenPosition(add); - } - - private void updateLastChosenPosition(ResolveInfo info) { - // If another profile is present, ignore the last chosen entry. - if (mOtherProfile != null) { - mLastChosenPosition = -1; - return; - } - if (mLastChosen != null - && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) - && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { - mLastChosenPosition = mDisplayList.size() - 1; - } - } - - // We assume that at this point we've already filtered out the only intent for a different - // targetUserId which we're going to use. - private void addResolveInfo(DisplayResolveInfo dri) { - if (dri != null && dri.mResolveInfo != null - && dri.mResolveInfo.targetUserId == UserHandle.USER_CURRENT) { - // Checks if this info is already listed in display. - for (DisplayResolveInfo existingInfo : mDisplayList) { - if (resolveInfoMatch(dri.mResolveInfo, existingInfo.mResolveInfo)) { - return; - } - } - mDisplayList.add(dri); - } - } - - @Nullable - public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { - TargetInfo target = targetInfoForPosition(position, filtered); - if (target != null) { - return target.getResolveInfo(); - } - return null; - } - - @Nullable - public TargetInfo targetInfoForPosition(int position, boolean filtered) { - if (filtered) { - return getItem(position); - } - if (mDisplayList.size() > position) { - return mDisplayList.get(position); - } - return null; - } - - public int getCount() { - int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : - mDisplayList.size(); - if (mFilterLastUsed && mLastChosenPosition >= 0) { - totalSize--; - } - return totalSize; - } - - public int getUnfilteredCount() { - return mDisplayList.size(); - } - - @Nullable - public TargetInfo getItem(int position) { - if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { - position++; - } - if (mDisplayList.size() > position) { - return mDisplayList.get(position); - } else { - return null; - } - } - - public long getItemId(int position) { - return position; - } - - public int getDisplayResolveInfoCount() { - return mDisplayList.size(); - } - - public DisplayResolveInfo getDisplayResolveInfo(int index) { - // Used to query services. We only query services for primary targets, not alternates. - return mDisplayList.get(index); - } - - public final View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - view = createView(parent); - } - onBindView(view, getItem(position)); - return view; - } - - public final View createView(ViewGroup parent) { - final View view = onCreateView(parent); - final ViewHolder holder = new ViewHolder(view); - view.setTag(holder); - return view; - } - - public View onCreateView(ViewGroup parent) { - return mInflater.inflate( - com.android.internal.R.layout.resolve_list_item, parent, false); - } - - public final void bindView(int position, View view) { - onBindView(view, getItem(position)); - } - - protected void onBindView(View view, TargetInfo info) { - final ViewHolder holder = (ViewHolder) view.getTag(); - if (info == null) { - holder.icon.setImageDrawable( - getDrawable(R.drawable.resolver_icon_placeholder)); - return; - } - - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayLabel()) { - getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); - } else { - holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo()); - } - - if (info.isSuspended()) { - holder.icon.setColorFilter(mSuspendedMatrixColorFilter); - } else { - holder.icon.setColorFilter(null); - } - - if (info instanceof DisplayResolveInfo - && !((DisplayResolveInfo) info).hasDisplayIcon()) { - new LoadIconTask((DisplayResolveInfo) info, holder.icon).execute(); - } else { - holder.icon.setImageDrawable(info.getDisplayIcon()); - } - } - } - - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelTask(info, holder); } @VisibleForTesting @@ -2144,41 +1280,6 @@ public class ResolverActivity extends Activity { } } - static class ViewHolder { - public View itemView; - public Drawable defaultItemViewBackground; - - public TextView text; - public TextView text2; - public ImageView icon; - - public ViewHolder(View view) { - itemView = view; - defaultItemViewBackground = view.getBackground(); - text = (TextView) view.findViewById(com.android.internal.R.id.text1); - text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); - icon = (ImageView) view.findViewById(R.id.icon); - } - - public void bindLabel(CharSequence label, CharSequence subLabel) { - if (!TextUtils.equals(text.getText(), label)) { - text.setText(label); - } - - // Always show a subLabel for visual consistency across list items. Show an empty - // subLabel if the subLabel is the same as the label - if (TextUtils.equals(label, subLabel)) { - subLabel = null; - } - - if (!TextUtils.equals(text2.getText(), subLabel) - && !TextUtils.isEmpty(subLabel)) { - text2.setVisibility(View.VISIBLE); - text2.setText(subLabel); - } - } - } - class ItemClickListener implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { @Override @@ -2229,61 +1330,6 @@ public class ResolverActivity extends Activity { } - protected class LoadLabelTask extends AsyncTask { - private final DisplayResolveInfo mDisplayResolveInfo; - private final ViewHolder mHolder; - - protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { - mDisplayResolveInfo = dri; - mHolder = holder; - } - - - @Override - protected CharSequence[] doInBackground(Void... voids) { - ResolveInfoPresentationGetter pg = - makePresentationGetter(mDisplayResolveInfo.mResolveInfo); - return new CharSequence[] { - pg.getLabel(), - pg.getSubLabel() - }; - } - - @Override - protected void onPostExecute(CharSequence[] result) { - mDisplayResolveInfo.setDisplayLabel(result[0]); - mDisplayResolveInfo.setExtendedInfo(result[1]); - mHolder.bindLabel(result[0], result[1]); - } - } - - class LoadIconTask extends AsyncTask { - protected final DisplayResolveInfo mDisplayResolveInfo; - private final ResolveInfo mResolveInfo; - private final ImageView mTargetView; - - LoadIconTask(DisplayResolveInfo dri, ImageView target) { - mDisplayResolveInfo = dri; - mResolveInfo = dri.getResolveInfo(); - mTargetView = target; - } - - @Override - protected Drawable doInBackground(Void... params) { - return loadIconForResolveInfo(mResolveInfo); - } - - @Override - protected void onPostExecute(Drawable d) { - if (mAdapter.getOtherProfile() == mDisplayResolveInfo) { - bindProfileView(); - } else { - mDisplayResolveInfo.setDisplayIcon(d); - mTargetView.setImageDrawable(d); - } - } - } - static final boolean isSpecificUriMatch(int match) { match = match&IntentFilter.MATCH_CATEGORY_MASK; return match >= IntentFilter.MATCH_CATEGORY_HOST diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java new file mode 100644 index 0000000000000..4076ddaa71b00 --- /dev/null +++ b/core/java/com/android/internal/app/ResolverListAdapter.java @@ -0,0 +1,862 @@ +/* + * Copyright (C) 2019 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 static android.content.Context.ACTIVITY_SERVICE; + +import android.annotation.NonNull; +import android.annotation.Nullable; +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.LabeledIntent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.TargetInfo; + +import java.util.ArrayList; +import java.util.List; + +public class ResolverListAdapter extends BaseAdapter { + private static final String TAG = "ResolverListAdapter"; + + private final List mIntents; + private final Intent[] mInitialIntents; + private final List mBaseResolveList; + private final PackageManager mPm; + protected final Context mContext; + private final ColorMatrixColorFilter mSuspendedMatrixColorFilter; + private final boolean mUseLayoutForBrowsables; + private final int mIconDpi; + protected ResolveInfo mLastChosen; + private DisplayResolveInfo mOtherProfile; + ResolverListController mResolverListController; + private int mPlaceholderCount; + private boolean mAllTargetsAreBrowsers = false; + + protected final LayoutInflater mInflater; + + // This one is the list that the Adapter will actually present. + List mDisplayList; + List mUnfilteredResolveList; + + private int mLastChosenPosition = -1; + private boolean mFilterLastUsed; + private final ResolverListCommunicator mResolverListCommunicator; + private Runnable mPostListReadyRunnable; + + public ResolverListAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, + boolean filterLastUsed, + ResolverListController resolverListController, + boolean useLayoutForBrowsables, + ResolverListCommunicator resolverListCommunicator) { + mContext = context; + mIntents = payloadIntents; + mInitialIntents = initialIntents; + mBaseResolveList = rList; + mInflater = LayoutInflater.from(context); + mPm = context.getPackageManager(); + mDisplayList = new ArrayList<>(); + mFilterLastUsed = filterLastUsed; + mResolverListController = resolverListController; + mSuspendedMatrixColorFilter = createSuspendedColorMatrix(); + mUseLayoutForBrowsables = useLayoutForBrowsables; + mResolverListCommunicator = resolverListCommunicator; + final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); + mIconDpi = am.getLauncherLargeIconDensity(); + } + + public void handlePackagesChanged() { + rebuildList(); + mResolverListCommunicator.onHandlePackagesChanged(); + } + + public void setPlaceholderCount(int count) { + mPlaceholderCount = count; + } + + public int getPlaceholderCount() { + return mPlaceholderCount; + } + + @Nullable + public DisplayResolveInfo getFilteredItem() { + if (mFilterLastUsed && mLastChosenPosition >= 0) { + // Not using getItem since it offsets to dodge this position for the list + return mDisplayList.get(mLastChosenPosition); + } + return null; + } + + public DisplayResolveInfo getOtherProfile() { + return mOtherProfile; + } + + public int getFilteredPosition() { + if (mFilterLastUsed && mLastChosenPosition >= 0) { + return mLastChosenPosition; + } + return AbsListView.INVALID_POSITION; + } + + public boolean hasFilteredItem() { + return mFilterLastUsed && mLastChosen != null; + } + + public float getScore(DisplayResolveInfo target) { + return mResolverListController.getScore(target); + } + + public void updateModel(ComponentName componentName) { + mResolverListController.updateModel(componentName); + } + + public void updateChooserCounts(String packageName, int userId, String action) { + mResolverListController.updateChooserCounts(packageName, userId, action); + } + + /** + * @return true if all items in the display list are defined as browsers by + * ResolveInfo.handleAllWebDataURI + */ + public boolean areAllTargetsBrowsers() { + return mAllTargetsAreBrowsers; + } + + /** + * 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; + // Clear the value of mOtherProfile from previous call. + mOtherProfile = null; + mLastChosen = null; + mLastChosenPosition = -1; + mAllTargetsAreBrowsers = false; + mDisplayList.clear(); + if (mBaseResolveList != null) { + currentResolveList = mUnfilteredResolveList = new ArrayList<>(); + mResolverListController.addResolveListDedupe(currentResolveList, + mResolverListCommunicator.getTargetIntent(), + mBaseResolveList); + } else { + currentResolveList = mUnfilteredResolveList = + mResolverListController.getResolversForIntent(shouldGetResolvedFilter(), + mResolverListCommunicator.shouldGetActivityMetadata(), + mIntents); + if (currentResolveList == null) { + processSortedList(currentResolveList); + return true; + } + List originalList = + mResolverListController.filterIneligibleActivities(currentResolveList, + true); + if (originalList != null) { + mUnfilteredResolveList = originalList; + } + } + + // So far we only support a single other profile at a time. + // The first one we see gets special treatment. + for (ResolvedComponentInfo info : currentResolveList) { + ResolveInfo resolveInfo = info.getResolveInfoAt(0); + if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { + Intent pOrigIntent = mResolverListCommunicator.getReplacementIntent( + resolveInfo.activityInfo, + info.getIntentAt(0)); + Intent replacementIntent = mResolverListCommunicator.getReplacementIntent( + resolveInfo.activityInfo, + mResolverListCommunicator.getTargetIntent()); + mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), + resolveInfo, + resolveInfo.loadLabel(mPm), + resolveInfo.loadLabel(mPm), + pOrigIntent != null ? pOrigIntent : replacementIntent, + makePresentationGetter(resolveInfo)); + currentResolveList.remove(info); + break; + } + } + + if (mOtherProfile == null) { + try { + mLastChosen = mResolverListController.getLastChosen(); + } catch (RemoteException re) { + Log.d(TAG, "Error calling getLastChosenActivity\n" + re); + } + } + + int n; + if ((currentResolveList != null) && ((n = currentResolveList.size()) > 0)) { + // 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 (currentResolveList.size() > 1) { + int placeholderCount = currentResolveList.size(); + if (mResolverListCommunicator.useLayoutWithDefault()) { + --placeholderCount; + } + setPlaceholderCount(placeholderCount); + createSortingTask().execute(currentResolveList); + postListReadyRunnable(); + return false; + } else { + processSortedList(currentResolveList); + return true; + } + } else { + processSortedList(currentResolveList); + return true; + } + } + + AsyncTask, + Void, + List> createSortingTask() { + return 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); + mResolverListCommunicator.updateProfileViewButton(); + notifyDataSetChanged(); + } + }; + } + + + protected void processSortedList(List sortedComponents) { + int n; + if (sortedComponents != null && (n = sortedComponents.size()) != 0) { + mAllTargetsAreBrowsers = mUseLayoutForBrowsables; + + // First put the initial items at the top. + if (mInitialIntents != null) { + for (int i = 0; i < mInitialIntents.length; i++) { + Intent ii = mInitialIntents[i]; + if (ii == null) { + continue; + } + ActivityInfo ai = ii.resolveActivityInfo( + mPm, 0); + if (ai == null) { + Log.w(TAG, "No activity found for " + ii); + continue; + } + ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = ai; + UserManager userManager = + (UserManager) mContext.getSystemService(Context.USER_SERVICE); + if (ii instanceof LabeledIntent) { + LabeledIntent li = (LabeledIntent) ii; + ri.resolvePackageName = li.getSourcePackage(); + ri.labelRes = li.getLabelResource(); + ri.nonLocalizedLabel = li.getNonLocalizedLabel(); + ri.icon = li.getIconResource(); + ri.iconResourceId = ri.icon; + } + if (userManager.isManagedProfile()) { + ri.noResourceId = true; + ri.icon = 0; + } + mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; + + addResolveInfo(new DisplayResolveInfo(ii, ri, + ri.loadLabel(mPm), null, ii, makePresentationGetter(ri))); + } + } + + + for (ResolvedComponentInfo rci : sortedComponents) { + final ResolveInfo ri = rci.getResolveInfoAt(0); + if (ri != null) { + mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; + addResolveInfoWithAlternates(rci); + } + } + } + + mResolverListCommunicator.sendVoiceChoicesIfNeeded(); + postListReadyRunnable(); + } + + /** + * Some necessary methods for creating the list are initiated in onCreate and will also + * determine the layout known. We therefore can't update the UI inline and post to the + * handler thread to update after the current task is finished. + */ + private void postListReadyRunnable() { + if (mPostListReadyRunnable == null) { + mPostListReadyRunnable = new Runnable() { + @Override + public void run() { + mResolverListCommunicator.onPostListReady(); + mPostListReadyRunnable = null; + } + }; + mContext.getMainThreadHandler().post(mPostListReadyRunnable); + } + } + + public boolean shouldGetResolvedFilter() { + return mFilterLastUsed; + } + + private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { + final int count = rci.getCount(); + final Intent intent = rci.getIntentAt(0); + final ResolveInfo add = rci.getResolveInfoAt(0); + final Intent replaceIntent = + mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent); + final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent( + add.activityInfo, mResolverListCommunicator.getTargetIntent()); + final DisplayResolveInfo + dri = new DisplayResolveInfo(intent, add, + replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add)); + addResolveInfo(dri); + if (replaceIntent == intent) { + // Only add alternates if we didn't get a specific replacement from + // the caller. If we have one it trumps potential alternates. + for (int i = 1, n = count; i < n; i++) { + final Intent altIntent = rci.getIntentAt(i); + dri.addAlternateSourceIntent(altIntent); + } + } + updateLastChosenPosition(add); + } + + private void updateLastChosenPosition(ResolveInfo info) { + // If another profile is present, ignore the last chosen entry. + if (mOtherProfile != null) { + mLastChosenPosition = -1; + return; + } + if (mLastChosen != null + && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) + && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { + mLastChosenPosition = mDisplayList.size() - 1; + } + } + + // We assume that at this point we've already filtered out the only intent for a different + // targetUserId which we're going to use. + private void addResolveInfo(DisplayResolveInfo dri) { + if (dri != null && dri.getResolveInfo() != null + && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) { + // Checks if this info is already listed in display. + for (DisplayResolveInfo existingInfo : mDisplayList) { + if (mResolverListCommunicator + .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { + return; + } + } + mDisplayList.add(dri); + } + } + + @Nullable + public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { + TargetInfo target = targetInfoForPosition(position, filtered); + if (target != null) { + return target.getResolveInfo(); + } + return null; + } + + @Nullable + public TargetInfo targetInfoForPosition(int position, boolean filtered) { + if (filtered) { + return getItem(position); + } + if (mDisplayList.size() > position) { + return mDisplayList.get(position); + } + return null; + } + + public int getCount() { + int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : + mDisplayList.size(); + if (mFilterLastUsed && mLastChosenPosition >= 0) { + totalSize--; + } + return totalSize; + } + + public int getUnfilteredCount() { + return mDisplayList.size(); + } + + @Nullable + public TargetInfo getItem(int position) { + if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { + position++; + } + if (mDisplayList.size() > position) { + return mDisplayList.get(position); + } else { + return null; + } + } + + public long getItemId(int position) { + return position; + } + + public int getDisplayResolveInfoCount() { + return mDisplayList.size(); + } + + public DisplayResolveInfo getDisplayResolveInfo(int index) { + // Used to query services. We only query services for primary targets, not alternates. + return mDisplayList.get(index); + } + + public final View getView(int position, View convertView, ViewGroup parent) { + View view = convertView; + if (view == null) { + view = createView(parent); + } + onBindView(view, getItem(position)); + return view; + } + + public final View createView(ViewGroup parent) { + final View view = onCreateView(parent); + final ViewHolder holder = new ViewHolder(view); + view.setTag(holder); + return view; + } + + public View onCreateView(ViewGroup parent) { + return mInflater.inflate( + com.android.internal.R.layout.resolve_list_item, parent, false); + } + + public final void bindView(int position, View view) { + onBindView(view, getItem(position)); + } + + protected void onBindView(View view, TargetInfo info) { + final ViewHolder holder = (ViewHolder) view.getTag(); + if (info == null) { + holder.icon.setImageDrawable( + mContext.getDrawable(R.drawable.resolver_icon_placeholder)); + return; + } + + if (info instanceof DisplayResolveInfo + && !((DisplayResolveInfo) info).hasDisplayLabel()) { + getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); + } else { + holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo()); + } + + if (info.isSuspended()) { + holder.icon.setColorFilter(mSuspendedMatrixColorFilter); + } else { + holder.icon.setColorFilter(null); + } + + if (info instanceof DisplayResolveInfo + && !((DisplayResolveInfo) info).hasDisplayIcon()) { + new ResolverListAdapter.LoadIconTask((DisplayResolveInfo) info, holder.icon).execute(); + } else { + holder.icon.setImageDrawable(info.getDisplayIcon(mContext)); + } + } + + protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { + return new LoadLabelTask(info, holder); + } + + public void onDestroy() { + if (mPostListReadyRunnable != null) { + mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); + mPostListReadyRunnable = null; + } + if (mResolverListController != null) { + mResolverListController.destroy(); + } + } + + private ColorMatrixColorFilter createSuspendedColorMatrix() { + int grayValue = 127; + float scale = 0.5f; // half bright + + ColorMatrix tempBrightnessMatrix = new ColorMatrix(); + float[] mat = tempBrightnessMatrix.getArray(); + mat[0] = scale; + mat[6] = scale; + mat[12] = scale; + mat[4] = grayValue; + mat[9] = grayValue; + mat[14] = grayValue; + + ColorMatrix matrix = new ColorMatrix(); + matrix.setSaturation(0.0f); + matrix.preConcat(tempBrightnessMatrix); + return new ColorMatrixColorFilter(matrix); + } + + ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { + return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai); + } + + ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { + return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri); + } + + Drawable loadIconForResolveInfo(ResolveInfo ri) { + // Load icons based on the current process. If in work profile icons should be badged. + return makePresentationGetter(ri).getIcon(Process.myUserHandle()); + } + + void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { + final DisplayResolveInfo iconInfo = getFilteredItem(); + if (iconView != null && iconInfo != null) { + new LoadIconTask(iconInfo, iconView).execute(); + } + } + + /** + * Necessary methods to communicate between {@link ResolverListAdapter} + * and {@link ResolverActivity}. + */ + interface ResolverListCommunicator { + + boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs); + + Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); + + void onPostListReady(); + + void sendVoiceChoicesIfNeeded(); + + void updateProfileViewButton(); + + boolean useLayoutWithDefault(); + + boolean shouldGetActivityMetadata(); + + Intent getTargetIntent(); + + void onHandlePackagesChanged(); + } + + static class ViewHolder { + public View itemView; + public Drawable defaultItemViewBackground; + + public TextView text; + public TextView text2; + public ImageView icon; + + ViewHolder(View view) { + itemView = view; + defaultItemViewBackground = view.getBackground(); + text = (TextView) view.findViewById(com.android.internal.R.id.text1); + text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); + icon = (ImageView) view.findViewById(R.id.icon); + } + + public void bindLabel(CharSequence label, CharSequence subLabel) { + if (!TextUtils.equals(text.getText(), label)) { + text.setText(label); + } + + // Always show a subLabel for visual consistency across list items. Show an empty + // subLabel if the subLabel is the same as the label + if (TextUtils.equals(label, subLabel)) { + subLabel = null; + } + + if (!TextUtils.equals(text2.getText(), subLabel) + && !TextUtils.isEmpty(subLabel)) { + text2.setVisibility(View.VISIBLE); + text2.setText(subLabel); + } + } + } + + protected class LoadLabelTask extends AsyncTask { + private final DisplayResolveInfo mDisplayResolveInfo; + private final ViewHolder mHolder; + + protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { + mDisplayResolveInfo = dri; + mHolder = holder; + } + + @Override + protected CharSequence[] doInBackground(Void... voids) { + ResolveInfoPresentationGetter pg = + makePresentationGetter(mDisplayResolveInfo.getResolveInfo()); + return new CharSequence[] { + pg.getLabel(), + pg.getSubLabel() + }; + } + + @Override + protected void onPostExecute(CharSequence[] result) { + mDisplayResolveInfo.setDisplayLabel(result[0]); + mDisplayResolveInfo.setExtendedInfo(result[1]); + mHolder.bindLabel(result[0], result[1]); + } + } + + class LoadIconTask extends AsyncTask { + protected final com.android.internal.app.chooser.DisplayResolveInfo mDisplayResolveInfo; + private final ResolveInfo mResolveInfo; + private final ImageView mTargetView; + + LoadIconTask(DisplayResolveInfo dri, ImageView target) { + mDisplayResolveInfo = dri; + mResolveInfo = dri.getResolveInfo(); + mTargetView = target; + } + + @Override + protected Drawable doInBackground(Void... params) { + return loadIconForResolveInfo(mResolveInfo); + } + + @Override + protected void onPostExecute(Drawable d) { + if (getOtherProfile() == mDisplayResolveInfo) { + mResolverListCommunicator.updateProfileViewButton(); + } else { + mDisplayResolveInfo.setDisplayIcon(d); + mTargetView.setImageDrawable(d); + } + } + } + + /** + * Loads the icon and label for the provided ResolveInfo. + */ + @VisibleForTesting + public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { + private final ResolveInfo mRi; + public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { + super(ctx, iconDpi, ri.activityInfo); + mRi = ri; + } + + @Override + Drawable getIconSubstituteInternal() { + Drawable dr = null; + try { + // Do not use ResolveInfo#getIconResource() as it defaults to the app + if (mRi.resolvePackageName != null && mRi.icon != 0) { + dr = loadIconFromResource( + mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " + + "couldn't find resources for package", e); + } + + // Fall back to ActivityInfo if no icon is found via ResolveInfo + if (dr == null) dr = super.getIconSubstituteInternal(); + + return dr; + } + + @Override + String getAppSubLabelInternal() { + // Will default to app name if no intent filter or activity label set, make sure to + // check if subLabel matches label before final display + return (String) mRi.loadLabel(mPm); + } + } + + /** + * Loads the icon and label for the provided ActivityInfo. + */ + @VisibleForTesting + public static class ActivityInfoPresentationGetter extends + TargetPresentationGetter { + private final ActivityInfo mActivityInfo; + public ActivityInfoPresentationGetter(Context ctx, int iconDpi, + ActivityInfo activityInfo) { + super(ctx, iconDpi, activityInfo.applicationInfo); + mActivityInfo = activityInfo; + } + + @Override + Drawable getIconSubstituteInternal() { + Drawable dr = null; + try { + // Do not use ActivityInfo#getIconResource() as it defaults to the app + if (mActivityInfo.icon != 0) { + dr = loadIconFromResource( + mPm.getResourcesForApplication(mActivityInfo.applicationInfo), + mActivityInfo.icon); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " + + "couldn't find resources for package", e); + } + + return dr; + } + + @Override + String getAppSubLabelInternal() { + // Will default to app name if no activity label set, make sure to check if subLabel + // matches label before final display + return (String) mActivityInfo.loadLabel(mPm); + } + } + + /** + * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application + * icon and label over any IntentFilter or Activity icon to increase user understanding, with an + * exception for applications that hold the right permission. Always attempts to use available + * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses + * Strings to strip creative formatting. + */ + private abstract static class TargetPresentationGetter { + @Nullable abstract Drawable getIconSubstituteInternal(); + @Nullable abstract String getAppSubLabelInternal(); + + private Context mCtx; + private final int mIconDpi; + private final boolean mHasSubstitutePermission; + private final ApplicationInfo mAi; + + protected PackageManager mPm; + + TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { + mCtx = ctx; + mPm = ctx.getPackageManager(); + mAi = ai; + mIconDpi = iconDpi; + mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( + android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, + mAi.packageName); + } + + public Drawable getIcon(UserHandle userHandle) { + return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); + } + + public Bitmap getIconBitmap(UserHandle userHandle) { + Drawable dr = null; + if (mHasSubstitutePermission) { + dr = getIconSubstituteInternal(); + } + + if (dr == null) { + try { + if (mAi.icon != 0) { + dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); + } + } catch (PackageManager.NameNotFoundException ignore) { + } + } + + // Fall back to ApplicationInfo#loadIcon if nothing has been loaded + if (dr == null) { + dr = mAi.loadIcon(mPm); + } + + SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); + Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); + sif.recycle(); + + return icon; + } + + public String getLabel() { + String label = null; + // Apps with the substitute permission will always show the sublabel as their label + if (mHasSubstitutePermission) { + label = getAppSubLabelInternal(); + } + + if (label == null) { + label = (String) mAi.loadLabel(mPm); + } + + return label; + } + + public String getSubLabel() { + // Apps with the substitute permission will never have a sublabel + if (mHasSubstitutePermission) return null; + return getAppSubLabelInternal(); + } + + protected String loadLabelFromResource(Resources res, int resId) { + return res.getString(resId); + } + + @Nullable + protected Drawable loadIconFromResource(Resources res, int resId) { + return res.getDrawableForDensity(resId, mIconDpi); + } + + } +} diff --git a/core/java/com/android/internal/app/ResolverListController.java b/core/java/com/android/internal/app/ResolverListController.java index 28a8a86313723..6cc60b786e551 100644 --- a/core/java/com/android/internal/app/ResolverListController.java +++ b/core/java/com/android/internal/app/ResolverListController.java @@ -31,6 +31,7 @@ import android.os.RemoteException; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.chooser.DisplayResolveInfo; import java.util.ArrayList; import java.util.Collections; @@ -332,7 +333,7 @@ public class ResolverListController { } @VisibleForTesting - public float getScore(ResolverActivity.DisplayResolveInfo target) { + public float getScore(DisplayResolveInfo target) { return mResolverComparator.getScore(target.getResolvedComponentName()); } diff --git a/core/java/com/android/internal/app/SimpleIconFactory.java b/core/java/com/android/internal/app/SimpleIconFactory.java index 7a4e76fa8c173..d618cdf86865a 100644 --- a/core/java/com/android/internal/app/SimpleIconFactory.java +++ b/core/java/com/android/internal/app/SimpleIconFactory.java @@ -214,7 +214,7 @@ public class SimpleIconFactory { * @deprecated Do not use, functionality will be replaced by iconloader lib eventually. */ @Deprecated - Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) { + public Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) { // If no icon is provided use the system default if (icon == null) { icon = getFullResDefaultActivityIcon(mFillResIconDpi); diff --git a/core/java/com/android/internal/app/chooser/ChooserTargetInfo.java b/core/java/com/android/internal/app/chooser/ChooserTargetInfo.java new file mode 100644 index 0000000000000..a2d0953b56207 --- /dev/null +++ b/core/java/com/android/internal/app/chooser/ChooserTargetInfo.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2019 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.chooser; + +import android.service.chooser.ChooserTarget; +import android.text.TextUtils; + +/** + * A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the + * Direct Share deep link into an application. + */ +public interface ChooserTargetInfo extends TargetInfo { + float getModifiedScore(); + + ChooserTarget getChooserTarget(); + + /** + * Do not label as 'equals', since this doesn't quite work + * as intended with java 8. + */ + default boolean isSimilar(ChooserTargetInfo other) { + if (other == null) return false; + + ChooserTarget ct1 = getChooserTarget(); + ChooserTarget ct2 = other.getChooserTarget(); + + // If either is null, there is not enough info to make an informed decision + // about equality, so just exit + if (ct1 == null || ct2 == null) return false; + + if (ct1.getComponentName().equals(ct2.getComponentName()) + && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel()) + && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) { + return true; + } + + return false; + } +} diff --git a/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java new file mode 100644 index 0000000000000..c77444e949ed1 --- /dev/null +++ b/core/java/com/android/internal/app/chooser/DisplayResolveInfo.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2019 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.chooser; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.Activity; +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.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.internal.app.ResolverActivity; +import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter; + +import java.util.ArrayList; +import java.util.List; + +/** + * A TargetInfo plus additional information needed to render it (such as icon and label) and + * resolve it to an activity. + */ +public class DisplayResolveInfo implements TargetInfo { + // Temporary flag for new chooser delegate behavior. + private static final boolean ENABLE_CHOOSER_DELEGATE = true; + + private final ResolveInfo mResolveInfo; + private CharSequence mDisplayLabel; + private Drawable mDisplayIcon; + private CharSequence mExtendedInfo; + private final Intent mResolvedIntent; + private final List mSourceIntents = new ArrayList<>(); + private boolean mIsSuspended; + private ResolveInfoPresentationGetter mResolveInfoPresentationGetter; + + public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent, + ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent, + resolveInfoPresentationGetter); + } + + public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, + CharSequence pInfo, @NonNull Intent resolvedIntent, + @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + mSourceIntents.add(originalIntent); + mResolveInfo = pri; + mDisplayLabel = pLabel; + mExtendedInfo = pInfo; + mResolveInfoPresentationGetter = resolveInfoPresentationGetter; + + final Intent intent = new Intent(resolvedIntent); + intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT + | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); + final ActivityInfo ai = mResolveInfo.activityInfo; + intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name)); + + mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; + + mResolvedIntent = intent; + } + + private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags, + ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + mSourceIntents.addAll(other.getAllSourceIntents()); + mResolveInfo = other.mResolveInfo; + mDisplayLabel = other.mDisplayLabel; + mDisplayIcon = other.mDisplayIcon; + mExtendedInfo = other.mExtendedInfo; + mResolvedIntent = new Intent(other.mResolvedIntent); + mResolvedIntent.fillIn(fillInIntent, flags); + mResolveInfoPresentationGetter = resolveInfoPresentationGetter; + } + + public ResolveInfo getResolveInfo() { + return mResolveInfo; + } + + public CharSequence getDisplayLabel() { + if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) { + mDisplayLabel = mResolveInfoPresentationGetter.getLabel(); + mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel(); + } + return mDisplayLabel; + } + + public boolean hasDisplayLabel() { + return mDisplayLabel != null; + } + + public void setDisplayLabel(CharSequence displayLabel) { + mDisplayLabel = displayLabel; + } + + public void setExtendedInfo(CharSequence extendedInfo) { + mExtendedInfo = extendedInfo; + } + + public Drawable getDisplayIcon(Context context) { + return mDisplayIcon; + } + + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter); + } + + @Override + public List getAllSourceIntents() { + return mSourceIntents; + } + + public void addAlternateSourceIntent(Intent alt) { + mSourceIntents.add(alt); + } + + public void setDisplayIcon(Drawable icon) { + mDisplayIcon = icon; + } + + public boolean hasDisplayIcon() { + return mDisplayIcon != null; + } + + public CharSequence getExtendedInfo() { + return mExtendedInfo; + } + + public Intent getResolvedIntent() { + return mResolvedIntent; + } + + @Override + public ComponentName getResolvedComponentName() { + return new ComponentName(mResolveInfo.activityInfo.packageName, + mResolveInfo.activityInfo.name); + } + + @Override + public boolean start(Activity activity, Bundle options) { + activity.startActivity(mResolvedIntent, options); + return true; + } + + @Override + public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + if (ENABLE_CHOOSER_DELEGATE) { + return activity.startAsCallerImpl(mResolvedIntent, options, false, userId); + } else { + activity.startActivityAsCaller(mResolvedIntent, options, null, false, userId); + return true; + } + } + + @Override + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + activity.startActivityAsUser(mResolvedIntent, options, user); + return false; + } + + public boolean isSuspended() { + return mIsSuspended; + } +} diff --git a/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java new file mode 100644 index 0000000000000..22cbdaa662677 --- /dev/null +++ b/core/java/com/android/internal/app/chooser/NotSelectableTargetInfo.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 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.chooser; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.UserHandle; +import android.service.chooser.ChooserTarget; + +import com.android.internal.app.ResolverActivity; + +import java.util.List; + +/** + * Distinguish between targets that selectable by the user, vs those that are + * placeholders for the system while information is loading in an async manner. + */ +public abstract class NotSelectableTargetInfo implements ChooserTargetInfo { + + public Intent getResolvedIntent() { + return null; + } + + public ComponentName getResolvedComponentName() { + return null; + } + + public boolean start(Activity activity, Bundle options) { + return false; + } + + public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + return false; + } + + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + return false; + } + + public ResolveInfo getResolveInfo() { + return null; + } + + public CharSequence getDisplayLabel() { + return null; + } + + public CharSequence getExtendedInfo() { + return null; + } + + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return null; + } + + public List getAllSourceIntents() { + return null; + } + + public float getModifiedScore() { + return -0.1f; + } + + public ChooserTarget getChooserTarget() { + return null; + } + + public boolean isSuspended() { + return false; + } +} diff --git a/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java new file mode 100644 index 0000000000000..1cc4857b39fe3 --- /dev/null +++ b/core/java/com/android/internal/app/chooser/SelectableTargetInfo.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2019 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.chooser; + +import android.app.Activity; +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.LauncherApps; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.UserHandle; +import android.service.chooser.ChooserTarget; +import android.text.SpannableStringBuilder; +import android.util.Log; + +import com.android.internal.app.ChooserActivity; +import com.android.internal.app.ChooserFlags; +import com.android.internal.app.ResolverActivity; +import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; +import com.android.internal.app.SimpleIconFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Live target, currently selectable by the user. + * @see NotSelectableTargetInfo + */ +public final class SelectableTargetInfo implements ChooserTargetInfo { + private static final String TAG = "SelectableTargetInfo"; + + private final Context mContext; + private final DisplayResolveInfo mSourceInfo; + private final ResolveInfo mBackupResolveInfo; + private final ChooserTarget mChooserTarget; + private final String mDisplayLabel; + private final PackageManager mPm; + private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator; + private Drawable mBadgeIcon = null; + private CharSequence mBadgeContentDescription; + private Drawable mDisplayIcon; + private final Intent mFillInIntent; + private final int mFillInFlags; + private final float mModifiedScore; + private boolean mIsSuspended = false; + + public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo, + ChooserTarget chooserTarget, + float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator) { + mContext = context; + mSourceInfo = sourceInfo; + mChooserTarget = chooserTarget; + mModifiedScore = modifiedScore; + mPm = mContext.getPackageManager(); + mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator; + if (sourceInfo != null) { + final ResolveInfo ri = sourceInfo.getResolveInfo(); + if (ri != null) { + final ActivityInfo ai = ri.activityInfo; + if (ai != null && ai.applicationInfo != null) { + final PackageManager pm = mContext.getPackageManager(); + mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); + mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); + mIsSuspended = + (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; + } + } + } + // TODO(b/121287224): do this in the background thread, and only for selected targets + mDisplayIcon = getChooserTargetIconDrawable(chooserTarget); + + if (sourceInfo != null) { + mBackupResolveInfo = null; + } else { + mBackupResolveInfo = + mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0); + } + + mFillInIntent = null; + mFillInFlags = 0; + + mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle()); + } + + private SelectableTargetInfo(SelectableTargetInfo other, + Intent fillInIntent, int flags) { + mContext = other.mContext; + mPm = other.mPm; + mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator; + mSourceInfo = other.mSourceInfo; + mBackupResolveInfo = other.mBackupResolveInfo; + mChooserTarget = other.mChooserTarget; + mBadgeIcon = other.mBadgeIcon; + mBadgeContentDescription = other.mBadgeContentDescription; + mDisplayIcon = other.mDisplayIcon; + mFillInIntent = fillInIntent; + mFillInFlags = flags; + mModifiedScore = other.mModifiedScore; + + mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle()); + } + + private String sanitizeDisplayLabel(CharSequence label) { + SpannableStringBuilder sb = new SpannableStringBuilder(label); + sb.clearSpans(); + return sb.toString(); + } + + public boolean isSuspended() { + return mIsSuspended; + } + + /** + * Since ShortcutInfos are returned by ShortcutManager, we can cache the shortcuts and skip + * the call to LauncherApps#getShortcuts(ShortcutQuery). + */ + // TODO(121287224): Refactor code to apply the suggestion above + private Drawable getChooserTargetIconDrawable(ChooserTarget target) { + Drawable directShareIcon = null; + + // First get the target drawable and associated activity info + final Icon icon = target.getIcon(); + if (icon != null) { + directShareIcon = icon.loadDrawable(mContext); + } else if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS) { + Bundle extras = target.getIntentExtras(); + if (extras != null && extras.containsKey(Intent.EXTRA_SHORTCUT_ID)) { + CharSequence shortcutId = extras.getCharSequence(Intent.EXTRA_SHORTCUT_ID); + LauncherApps launcherApps = (LauncherApps) mContext.getSystemService( + Context.LAUNCHER_APPS_SERVICE); + final LauncherApps.ShortcutQuery q = new LauncherApps.ShortcutQuery(); + q.setPackage(target.getComponentName().getPackageName()); + q.setShortcutIds(Arrays.asList(shortcutId.toString())); + q.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC); + final List shortcuts = + launcherApps.getShortcuts(q, mContext.getUser()); + if (shortcuts != null && shortcuts.size() > 0) { + directShareIcon = launcherApps.getShortcutIconDrawable(shortcuts.get(0), 0); + } + } + } + + if (directShareIcon == null) return null; + + ActivityInfo info = null; + try { + info = mPm.getActivityInfo(target.getComponentName(), 0); + } catch (PackageManager.NameNotFoundException error) { + Log.e(TAG, "Could not find activity associated with ChooserTarget"); + } + + if (info == null) return null; + + // Now fetch app icon and raster with no badging even in work profile + Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info) + .getIconBitmap(UserHandle.getUserHandleForUid(UserHandle.myUserId())); + + // Raster target drawable with appIcon as a badge + SimpleIconFactory sif = SimpleIconFactory.obtain(mContext); + Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); + sif.recycle(); + + return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon); + } + + public float getModifiedScore() { + return mModifiedScore; + } + + @Override + public Intent getResolvedIntent() { + if (mSourceInfo != null) { + return mSourceInfo.getResolvedIntent(); + } + + final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent()); + targetIntent.setComponent(mChooserTarget.getComponentName()); + targetIntent.putExtras(mChooserTarget.getIntentExtras()); + return targetIntent; + } + + @Override + public ComponentName getResolvedComponentName() { + if (mSourceInfo != null) { + return mSourceInfo.getResolvedComponentName(); + } else if (mBackupResolveInfo != null) { + return new ComponentName(mBackupResolveInfo.activityInfo.packageName, + mBackupResolveInfo.activityInfo.name); + } + return null; + } + + private Intent getBaseIntentToSend() { + Intent result = getResolvedIntent(); + if (result == null) { + Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); + } else { + result = new Intent(result); + if (mFillInIntent != null) { + result.fillIn(mFillInIntent, mFillInFlags); + } + result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0); + } + return result; + } + + @Override + public boolean start(Activity activity, Bundle options) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } + + @Override + public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { + final Intent intent = getBaseIntentToSend(); + if (intent == null) { + return false; + } + intent.setComponent(mChooserTarget.getComponentName()); + intent.putExtras(mChooserTarget.getIntentExtras()); + + // Important: we will ignore the target security checks in ActivityManager + // if and only if the ChooserTarget's target package is the same package + // where we got the ChooserTargetService that provided it. This lets a + // ChooserTargetService provide a non-exported or permission-guarded target + // to the chooser for the user to pick. + // + // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere + // so we'll obey the caller's normal security checks. + final boolean ignoreTargetSecurity = mSourceInfo != null + && mSourceInfo.getResolvedComponentName().getPackageName() + .equals(mChooserTarget.getComponentName().getPackageName()); + return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId); + } + + @Override + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } + + @Override + public ResolveInfo getResolveInfo() { + return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; + } + + @Override + public CharSequence getDisplayLabel() { + return mDisplayLabel; + } + + @Override + public CharSequence getExtendedInfo() { + // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. + return null; + } + + @Override + public Drawable getDisplayIcon(Context context) { + return mDisplayIcon; + } + + public ChooserTarget getChooserTarget() { + return mChooserTarget; + } + + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new SelectableTargetInfo(this, fillInIntent, flags); + } + + @Override + public List getAllSourceIntents() { + final List results = new ArrayList<>(); + if (mSourceInfo != null) { + // We only queried the service for the first one in our sourceinfo. + results.add(mSourceInfo.getAllSourceIntents().get(0)); + } + return results; + } + + /** + * Necessary methods to communicate between {@link SelectableTargetInfo} + * and {@link ResolverActivity} or {@link ChooserActivity}. + */ + public interface SelectableTargetInfoCommunicator { + + ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info); + + Intent getTargetIntent(); + + Intent getReferrerFillInIntent(); + } +} diff --git a/core/java/com/android/internal/app/chooser/TargetInfo.java b/core/java/com/android/internal/app/chooser/TargetInfo.java new file mode 100644 index 0000000000000..b59def174828d --- /dev/null +++ b/core/java/com/android/internal/app/chooser/TargetInfo.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2019 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.chooser; + + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.internal.app.ResolverActivity; + +import java.util.List; + +/** + * A single target as represented in the chooser. + */ +public interface TargetInfo { + /** + * Get the resolved intent that represents this target. Note that this may not be the + * intent that will be launched by calling one of the start methods provided; + * this is the intent that will be credited with the launch. + * + * @return the resolved intent for this target + */ + Intent getResolvedIntent(); + + /** + * Get the resolved component name that represents this target. Note that this may not + * be the component that will be directly launched by calling one of the start + * methods provided; this is the component that will be credited with the launch. + * + * @return the resolved ComponentName for this target + */ + ComponentName getResolvedComponentName(); + + /** + * Start the activity referenced by this target. + * + * @param activity calling Activity performing the launch + * @param options ActivityOptions bundle + * @return true if the start completed successfully + */ + boolean start(Activity activity, Bundle options); + + /** + * Start the activity referenced by this target as if the ResolverActivity's caller + * was performing the start operation. + * + * @param activity calling Activity (actually) performing the launch + * @param options ActivityOptions bundle + * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller + * @return true if the start completed successfully + */ + boolean startAsCaller(ResolverActivity activity, Bundle options, int userId); + + /** + * Start the activity referenced by this target as a given user. + * + * @param activity calling activity performing the launch + * @param options ActivityOptions bundle + * @param user handle for the user to start the activity as + * @return true if the start completed successfully + */ + boolean startAsUser(Activity activity, Bundle options, UserHandle user); + + /** + * Return the ResolveInfo about how and why this target matched the original query + * for available targets. + * + * @return ResolveInfo representing this target's match + */ + ResolveInfo getResolveInfo(); + + /** + * Return the human-readable text label for this target. + * + * @return user-visible target label + */ + CharSequence getDisplayLabel(); + + /** + * Return any extended info for this target. This may be used to disambiguate + * otherwise identical targets. + * + * @return human-readable disambig string or null if none present + */ + CharSequence getExtendedInfo(); + + /** + * @return The drawable that should be used to represent this target including badge + * @param context + */ + Drawable getDisplayIcon(Context context); + + /** + * Clone this target with the given fill-in information. + */ + TargetInfo cloneFilledIn(Intent fillInIntent, int flags); + + /** + * @return the list of supported source intents deduped against this single target + */ + List getAllSourceIntents(); + + /** + * @return true if this target can be selected by the user + */ + boolean isSuspended(); +} diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 5ea91da98fd33..d427cbda7fb66 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -24,12 +24,12 @@ import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; -import static com.android.internal.app.ChooserActivity.CALLER_TARGET_SCORE_BOOST; -import static com.android.internal.app.ChooserActivity.SHORTCUT_TARGET_SCORE_BOOST; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_DEFAULT; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE; import static com.android.internal.app.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER; +import static com.android.internal.app.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST; +import static com.android.internal.app.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST; import static com.android.internal.app.ChooserWrapperActivity.sOverrides; import static org.hamcrest.CoreMatchers.is; @@ -69,6 +69,7 @@ import androidx.test.rule.ActivityTestRule; import com.android.internal.R; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; +import com.android.internal.app.chooser.DisplayResolveInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -819,17 +820,18 @@ public class ChooserActivityTest { when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), Mockito.anyBoolean(), Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); - when(sOverrides.resolverListController.getScore(Mockito.isA( - ResolverActivity.DisplayResolveInfo.class))).thenReturn(testBaseScore); + when(sOverrides.resolverListController.getScore(Mockito.isA(DisplayResolveInfo.class))) + .thenReturn(testBaseScore); final ChooserWrapperActivity activity = mActivityRule .launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - final ResolverActivity.DisplayResolveInfo testDri = + final DisplayResolveInfo testDri = activity.createTestDisplayResolveInfo(sendIntent, - ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent); - final ChooserActivity.ChooserListAdapter adapter = activity.getAdapter(); + ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent, + /* resolveInfoPresentationGetter */ null); + final ChooserListAdapter adapter = activity.getAdapter(); assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST)); assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore)); @@ -970,7 +972,8 @@ public class ChooserActivityTest { ri, "testLabel", "testInfo", - sendIntent), + sendIntent, + /* resolveInfoPresentationGetter */ null), serviceTargets, TARGET_TYPE_CHOOSER_TARGET) ); @@ -1036,7 +1039,8 @@ public class ChooserActivityTest { ri, "testLabel", "testInfo", - sendIntent), + sendIntent, + /* resolveInfoPresentationGetter */ null), serviceTargets, TARGET_TYPE_CHOOSER_TARGET) ); @@ -1097,7 +1101,8 @@ public class ChooserActivityTest { ri, "testLabel", "testInfo", - sendIntent), + sendIntent, + /* resolveInfoPresentationGetter */ null), serviceTargets, TARGET_TYPE_CHOOSER_TARGET) ); diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index 1d567c73f3761..03705d0599e50 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -18,6 +18,7 @@ package com.android.internal.app; import static org.mockito.Mockito.mock; +import android.annotation.Nullable; import android.app.usage.UsageStatsManager; import android.content.ContentResolver; import android.content.Context; @@ -30,6 +31,9 @@ import android.graphics.Bitmap; import android.net.Uri; import android.util.Size; +import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter; +import com.android.internal.app.chooser.DisplayResolveInfo; +import com.android.internal.app.chooser.TargetInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -64,7 +68,7 @@ public class ChooserWrapperActivity extends ChooserActivity { } @Override - public void safelyStartActivity(TargetInfo cti) { + public void safelyStartActivity(com.android.internal.app.chooser.TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { return; @@ -133,8 +137,10 @@ public class ChooserWrapperActivity extends ChooserActivity { } public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, - CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) { - return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, pOrigIntent); + CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, + @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent, + resolveInfoPresentationGetter); } /** diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index 0fa29bf4436a3..a401e21df8054 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -43,10 +43,10 @@ import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.internal.R; -import com.android.internal.app.ResolverActivity.ActivityInfoPresentationGetter; -import com.android.internal.app.ResolverActivity.ResolveInfoPresentationGetter; import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; import com.android.internal.app.ResolverDataProvider.PackageManagerMockedInfo; +import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; +import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.internal.widget.ResolverDrawerLayout; import org.junit.Before; @@ -83,7 +83,7 @@ public class ResolverActivityTest { Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); waitForIdle(); assertThat(activity.getAdapter().getCount(), is(2)); @@ -216,7 +216,7 @@ public class ResolverActivityTest { Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the last used slot @@ -254,7 +254,7 @@ public class ResolverActivityTest { Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot @@ -300,7 +300,7 @@ public class ResolverActivityTest { .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - Espresso.registerIdlingResources(activity.getLabelIdlingResource()); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); waitForIdle(); // The other entry is filtered to the other profile slot diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java index 9082543e8ebc2..39cc83c3bc433 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -19,10 +19,14 @@ package com.android.internal.app; import static org.mockito.Mockito.mock; import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; -import androidx.test.espresso.idling.CountingIdlingResource; +import com.android.internal.app.chooser.TargetInfo; +import java.util.List; import java.util.function.Function; /* @@ -31,15 +35,17 @@ import java.util.function.Function; public class ResolverWrapperActivity extends ResolverActivity { static final OverrideData sOverrides = new OverrideData(); private UsageStatsManager mUsm; - private CountingIdlingResource mLabelIdlingResource = - new CountingIdlingResource("LoadLabelTask"); - public CountingIdlingResource getLabelIdlingResource() { - return mLabelIdlingResource; + @Override + public ResolverListAdapter createAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, boolean filterLastUsed, + boolean useLayoutForBrowsables) { + return new ResolverWrapperAdapter(context, payloadIntents, initialIntents, rList, + filterLastUsed, createListController(), useLayoutForBrowsables, this); } - ResolveListAdapter getAdapter() { - return mAdapter; + ResolverWrapperAdapter getAdapter() { + return (ResolverWrapperAdapter) mAdapter; } @Override @@ -72,11 +78,6 @@ public class ResolverWrapperActivity extends ResolverActivity { return super.getPackageManager(); } - @Override - protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { - return new LoadLabelWrapperTask(info, holder); - } - /** * We cannot directly mock the activity created since instrumentation creates it. *

@@ -96,22 +97,4 @@ public class ResolverWrapperActivity extends ResolverActivity { resolverListController = mock(ResolverListController.class); } } - - class LoadLabelWrapperTask extends LoadLabelTask { - - protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) { - super(dri, holder); - } - - @Override - protected void onPreExecute() { - mLabelIdlingResource.increment(); - } - - @Override - protected void onPostExecute(CharSequence[] result) { - super.onPostExecute(result); - mLabelIdlingResource.decrement(); - } - } } \ No newline at end of file diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java new file mode 100644 index 0000000000000..e41df4186a12b --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2019 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.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; + +import androidx.test.espresso.idling.CountingIdlingResource; + +import com.android.internal.app.chooser.DisplayResolveInfo; + +import java.util.List; + +public class ResolverWrapperAdapter extends ResolverListAdapter { + + private CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); + + public ResolverWrapperAdapter(Context context, + List payloadIntents, + Intent[] initialIntents, + List rList, boolean filterLastUsed, + ResolverListController resolverListController, boolean useLayoutForBrowsables, + ResolverListCommunicator resolverListCommunicator) { + super(context, payloadIntents, initialIntents, rList, filterLastUsed, + resolverListController, + useLayoutForBrowsables, resolverListCommunicator); + } + + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + + @Override + protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { + return new LoadLabelWrapperTask(info, holder); + } + + class LoadLabelWrapperTask extends LoadLabelTask { + + protected LoadLabelWrapperTask(DisplayResolveInfo dri, ViewHolder holder) { + super(dri, holder); + } + + @Override + protected void onPreExecute() { + mLabelIdlingResource.increment(); + } + + @Override + protected void onPostExecute(CharSequence[] result) { + super.onPostExecute(result); + mLabelIdlingResource.decrement(); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java index fa3ff64e5e180..0b273274f86d3 100644 --- a/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java @@ -35,6 +35,7 @@ import android.util.Log; import android.widget.CheckBox; import com.android.internal.app.ResolverActivity; +import com.android.internal.app.chooser.TargetInfo; import com.android.systemui.R; import java.util.ArrayList;