diff options
| author | 2022-12-16 17:36:27 +0000 | |
|---|---|---|
| committer | 2022-12-16 17:36:27 +0000 | |
| commit | 4ebf21f578b78ed83de87f829c4a126c9bb40759 (patch) | |
| tree | f78dbe0381fcd8cf202afdfaa1ffef83885546d1 /java | |
| parent | 9ebd81953ce60236381a068a0b7c52c8164bbfea (diff) | |
| parent | 6e627be666fe43a9f13559856e158196d8ed7f4e (diff) | |
Merge "Extract TargetPresentationGetter hierarchy" into tm-qpr-dev
Diffstat (limited to 'java')
10 files changed, 513 insertions, 293 deletions
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 12a054b9..699190f9 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -224,7 +224,7 @@ public class ChooserListAdapter extends ResolverListAdapter { ri.icon = 0; } DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( - ii, ri, ii, makePresentationGetter(ri)); + ii, ri, ii, mPresentationFactory.makePresentationGetter(ri)); mCallerTargets.add(displayResolveInfo); if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break; } @@ -715,7 +715,7 @@ public class ChooserListAdapter extends ResolverListAdapter { } // Now fetch app icon and raster with no badging even in work profile - Bitmap appIcon = makePresentationGetter(info).getIconBitmap(null); + Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null); // Raster target drawable with appIcon as a badge SimpleIconFactory sif = SimpleIconFactory.obtain(context); diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java index f4d4a6d1..0aa32505 100644 --- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java +++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java @@ -19,8 +19,6 @@ package com.android.intentresolver; import static android.content.Context.ACTIVITY_SERVICE; -import static com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; - import static java.util.stream.Collectors.toList; import android.annotation.NonNull; @@ -136,7 +134,7 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment ImageView icon = v.findViewById(com.android.internal.R.id.icon); RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer); - final ResolveInfoPresentationGetter pg = getProvidingAppPresentationGetter(); + final TargetPresentationGetter pg = getProvidingAppPresentationGetter(); title.setText(isShortcutTarget() ? mShortcutTitle : pg.getLabel()); icon.setImageDrawable(pg.getIcon(mUserHandle)); rv.setAdapter(new VHAdapter(items)); @@ -270,14 +268,14 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment return getPinIcon(isPinned(dri)); } - private ResolveInfoPresentationGetter getProvidingAppPresentationGetter() { + private TargetPresentationGetter getProvidingAppPresentationGetter() { final ActivityManager am = (ActivityManager) getContext() .getSystemService(ACTIVITY_SERVICE); final int iconDpi = am.getLauncherLargeIconDensity(); // Use the matching application icon and label for the title, any TargetInfo will do - return new ResolveInfoPresentationGetter(getContext(), iconDpi, - mTargetInfos.get(0).getResolveInfo()); + return new TargetPresentationGetter.Factory(getContext(), iconDpi) + .makePresentationGetter(mTargetInfos.get(0).getResolveInfo()); } private boolean isPinned(DisplayResolveInfo dri) { diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 5513eda2..eecb914c 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -26,15 +26,11 @@ import android.content.Context; import android.content.Intent; import android.content.PermissionChecker; 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.RemoteException; @@ -74,6 +70,7 @@ public class ResolverListAdapter extends BaseAdapter { protected final LayoutInflater mInflater; protected final ResolverListCommunicator mResolverListCommunicator; protected final ResolverListController mResolverListController; + protected final TargetPresentationGetter.Factory mPresentationFactory; private final List<Intent> mIntents; private final Intent[] mInitialIntents; @@ -126,6 +123,7 @@ public class ResolverListAdapter extends BaseAdapter { mIsAudioCaptureDevice = isAudioCaptureDevice; final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); mIconDpi = am.getLauncherLargeIconDensity(); + mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi); } public final DisplayResolveInfo getFirstDisplayResolveInfo() { @@ -479,7 +477,7 @@ public class ResolverListAdapter extends BaseAdapter { ri.loadLabel(mPm), null, ii, - makePresentationGetter(ri))); + mPresentationFactory.makePresentationGetter(ri))); } } @@ -532,7 +530,7 @@ public class ResolverListAdapter extends BaseAdapter { intent, add, (replaceIntent != null) ? replaceIntent : defaultIntent, - makePresentationGetter(add)); + mPresentationFactory.makePresentationGetter(add)); dri.setPinned(rci.isPinned()); if (rci.isPinned()) { Log.i(TAG, "Pinned item: " + rci.name); @@ -765,17 +763,9 @@ public class ResolverListAdapter extends BaseAdapter { return sSuspendedMatrixColorFilter; } - 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(getUserHandle()); + return mPresentationFactory.makePresentationGetter(ri).getIcon(getUserHandle()); } protected final Drawable loadIconPlaceholder() { @@ -875,8 +865,9 @@ public class ResolverListAdapter extends BaseAdapter { Intent replacementIntent = resolverListCommunicator.getReplacementIntent( resolveInfo.activityInfo, targetIntent); - ResolveInfoPresentationGetter presentationGetter = - new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo); + TargetPresentationGetter presentationGetter = + new TargetPresentationGetter.Factory(context, iconDpi) + .makePresentationGetter(resolveInfo); return DisplayResolveInfo.newDisplayResolveInfo( resolvedComponentInfo.getIntentAt(0), @@ -979,8 +970,8 @@ public class ResolverListAdapter extends BaseAdapter { @Override protected CharSequence[] doInBackground(Void... voids) { - ResolveInfoPresentationGetter pg = - makePresentationGetter(mDisplayResolveInfo.getResolveInfo()); + TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter( + mDisplayResolveInfo.getResolveInfo()); if (mIsAudioCaptureDevice) { // This is an audio capture device, so check record permissions @@ -1051,194 +1042,4 @@ public class ResolverListAdapter extends BaseAdapter { } } } - - /** - * 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 mRi.loadLabel(mPm).toString(); - } - - @Override - String getAppLabelForSubstitutePermission() { - // Will default to app name if no activity label set - return mRi.getComponentInfo().loadLabel(mPm).toString(); - } - } - - /** - * 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); - } - - @Override - String getAppLabelForSubstitutePermission() { - return getAppSubLabelInternal(); - } - } - - /** - * 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(); - @Nullable abstract String getAppLabelForSubstitutePermission(); - - 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(@Nullable 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 activity label as the - // app label if provided - if (mHasSubstitutePermission) { - label = getAppLabelForSubstitutePermission(); - } - - if (label == null) { - label = (String) mAi.loadLabel(mPm); - } - - return label; - } - - public String getSubLabel() { - // Apps with the substitute permission will always show the resolve info label as the - // sublabel if provided - if (mHasSubstitutePermission){ - String appSubLabel = getAppSubLabelInternal(); - // Use the resolve info label as sublabel if it is set - if(!TextUtils.isEmpty(appSubLabel) - && !TextUtils.equals(appSubLabel, getLabel())){ - return appSubLabel; - } - 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/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java new file mode 100644 index 00000000..f8b36566 --- /dev/null +++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2022 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.intentresolver; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +/** + * 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. + * + * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the + * appropriate concrete type. + * + * TODO: once this component (and its tests) are merged, it should be possible to refactor and + * vastly simplify by precomputing conditional logic at initialization. + */ +public abstract class TargetPresentationGetter { + private static final String TAG = "ResolverListAdapter"; + + /** Helper to build appropriate type-specific {@link TargetPresentationGetter} instances. */ + public static class Factory { + private final Context mContext; + private final int mIconDpi; + + public Factory(Context context, int iconDpi) { + mContext = context; + mIconDpi = iconDpi; + } + + /** Make a {@link TargetPresentationGetter} for an {@link ActivityInfo}. */ + public TargetPresentationGetter makePresentationGetter(ActivityInfo activityInfo) { + return new ActivityInfoPresentationGetter(mContext, mIconDpi, activityInfo); + } + + /** Make a {@link TargetPresentationGetter} for a {@link ResolveInfo}. */ + public TargetPresentationGetter makePresentationGetter(ResolveInfo resolveInfo) { + return new ResolveInfoPresentationGetter(mContext, mIconDpi, resolveInfo); + } + } + + @Nullable + protected abstract Drawable getIconSubstituteInternal(); + + @Nullable + protected abstract String getAppSubLabelInternal(); + + @Nullable + protected abstract String getAppLabelForSubstitutePermission(); + + private Context mContext; + private final int mIconDpi; + private final boolean mHasSubstitutePermission; + private final ApplicationInfo mAppInfo; + + protected PackageManager mPm; + + /** + * Retrieve the image that should be displayed as the icon when this target is presented to the + * specified {@code userHandle}. + */ + public Drawable getIcon(UserHandle userHandle) { + return new BitmapDrawable(mContext.getResources(), getIconBitmap(userHandle)); + } + + /** + * Retrieve the image that should be displayed as the icon when this target is presented to the + * specified {@code userHandle}. + */ + public Bitmap getIconBitmap(@Nullable UserHandle userHandle) { + Drawable drawable = null; + if (mHasSubstitutePermission) { + drawable = getIconSubstituteInternal(); + } + + if (drawable == null) { + try { + if (mAppInfo.icon != 0) { + drawable = loadIconFromResource( + mPm.getResourcesForApplication(mAppInfo), mAppInfo.icon); + } + } catch (PackageManager.NameNotFoundException ignore) { } + } + + // Fall back to ApplicationInfo#loadIcon if nothing has been loaded + if (drawable == null) { + drawable = mAppInfo.loadIcon(mPm); + } + + SimpleIconFactory iconFactory = SimpleIconFactory.obtain(mContext); + Bitmap icon = iconFactory.createUserBadgedIconBitmap(drawable, userHandle); + iconFactory.recycle(); + + return icon; + } + + /** Get the label to display for the target. */ + public String getLabel() { + String label = null; + // Apps with the substitute permission will always show the activity label as the app label + // if provided. + if (mHasSubstitutePermission) { + label = getAppLabelForSubstitutePermission(); + } + + if (label == null) { + label = (String) mAppInfo.loadLabel(mPm); + } + + return label; + } + + /** + * Get the sublabel to display for the target. Clients are responsible for deduping their + * presentation if this returns the same value as {@link #getLabel()}. + * TODO: this class should take responsibility for that deduping internally so it's an + * authoritative record of exactly the content that should be presented. + */ + public String getSubLabel() { + // Apps with the substitute permission will always show the resolve info label as the + // sublabel if provided + if (mHasSubstitutePermission) { + String appSubLabel = getAppSubLabelInternal(); + // Use the resolve info label as sublabel if it is set + if (!TextUtils.isEmpty(appSubLabel) && !TextUtils.equals(appSubLabel, getLabel())) { + return appSubLabel; + } + 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); + } + + private TargetPresentationGetter(Context context, int iconDpi, ApplicationInfo appInfo) { + mContext = context; + mPm = context.getPackageManager(); + mAppInfo = appInfo; + mIconDpi = iconDpi; + mHasSubstitutePermission = (PackageManager.PERMISSION_GRANTED == mPm.checkPermission( + android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, + mAppInfo.packageName)); + } + + /** Loads the icon and label for the provided ResolveInfo. */ + private static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { + private final ResolveInfo mResolveInfo; + + ResolveInfoPresentationGetter( + Context context, int iconDpi, ResolveInfo resolveInfo) { + super(context, iconDpi, resolveInfo.activityInfo); + mResolveInfo = resolveInfo; + } + + @Override + protected Drawable getIconSubstituteInternal() { + Drawable drawable = null; + try { + // Do not use ResolveInfo#getIconResource() as it defaults to the app + if (mResolveInfo.resolvePackageName != null && mResolveInfo.icon != 0) { + drawable = loadIconFromResource( + mPm.getResourcesForApplication(mResolveInfo.resolvePackageName), + mResolveInfo.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 (drawable == null) { + drawable = super.getIconSubstituteInternal(); + } + + return drawable; + } + + @Override + protected 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 mResolveInfo.loadLabel(mPm).toString(); + } + + @Override + protected String getAppLabelForSubstitutePermission() { + // Will default to app name if no activity label set + return mResolveInfo.getComponentInfo().loadLabel(mPm).toString(); + } + } + + /** Loads the icon and label for the provided {@link ActivityInfo}. */ + private static class ActivityInfoPresentationGetter extends TargetPresentationGetter { + private final ActivityInfo mActivityInfo; + + ActivityInfoPresentationGetter( + Context context, int iconDpi, ActivityInfo activityInfo) { + super(context, iconDpi, activityInfo.applicationInfo); + mActivityInfo = activityInfo; + } + + @Override + protected Drawable getIconSubstituteInternal() { + Drawable drawable = null; + try { + // Do not use ActivityInfo#getIconResource() as it defaults to the app + if (mActivityInfo.icon != 0) { + drawable = 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 drawable; + } + + @Override + protected 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); + } + + @Override + protected String getAppLabelForSubstitutePermission() { + return getAppSubLabelInternal(); + } + } +} diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java index c1b007af..db5ae0b4 100644 --- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java +++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java @@ -28,7 +28,7 @@ import android.os.Bundle; import android.os.UserHandle; import com.android.intentresolver.ResolverActivity; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; +import com.android.intentresolver.TargetPresentationGetter; import java.util.ArrayList; import java.util.Arrays; @@ -45,7 +45,7 @@ public class DisplayResolveInfo implements TargetInfo { private final Intent mResolvedIntent; private final List<Intent> mSourceIntents = new ArrayList<>(); private final boolean mIsSuspended; - private ResolveInfoPresentationGetter mResolveInfoPresentationGetter; + private TargetPresentationGetter mPresentationGetter; private boolean mPinned = false; private final IconHolder mDisplayIconHolder = new SettableIconHolder(); @@ -54,7 +54,7 @@ public class DisplayResolveInfo implements TargetInfo { Intent originalIntent, ResolveInfo resolveInfo, @NonNull Intent resolvedIntent, - @Nullable ResolveInfoPresentationGetter presentationGetter) { + @Nullable TargetPresentationGetter presentationGetter) { return newDisplayResolveInfo( originalIntent, resolveInfo, @@ -71,14 +71,14 @@ public class DisplayResolveInfo implements TargetInfo { CharSequence displayLabel, CharSequence extendedInfo, @NonNull Intent resolvedIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + @Nullable TargetPresentationGetter presentationGetter) { return new DisplayResolveInfo( originalIntent, resolveInfo, displayLabel, extendedInfo, resolvedIntent, - resolveInfoPresentationGetter); + presentationGetter); } private DisplayResolveInfo( @@ -87,12 +87,12 @@ public class DisplayResolveInfo implements TargetInfo { CharSequence displayLabel, CharSequence extendedInfo, @NonNull Intent resolvedIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + @Nullable TargetPresentationGetter presentationGetter) { mSourceIntents.add(originalIntent); mResolveInfo = resolveInfo; mDisplayLabel = displayLabel; mExtendedInfo = extendedInfo; - mResolveInfoPresentationGetter = resolveInfoPresentationGetter; + mPresentationGetter = presentationGetter; final ActivityInfo ai = mResolveInfo.activityInfo; mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; @@ -108,7 +108,7 @@ public class DisplayResolveInfo implements TargetInfo { DisplayResolveInfo other, Intent fillInIntent, int flags, - ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + TargetPresentationGetter presentationGetter) { mSourceIntents.addAll(other.getAllSourceIntents()); mResolveInfo = other.mResolveInfo; mIsSuspended = other.mIsSuspended; @@ -116,7 +116,7 @@ public class DisplayResolveInfo implements TargetInfo { mExtendedInfo = other.mExtendedInfo; mResolvedIntent = new Intent(other.mResolvedIntent); mResolvedIntent.fillIn(fillInIntent, flags); - mResolveInfoPresentationGetter = resolveInfoPresentationGetter; + mPresentationGetter = presentationGetter; mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon()); } @@ -128,7 +128,7 @@ public class DisplayResolveInfo implements TargetInfo { mDisplayLabel = other.mDisplayLabel; mExtendedInfo = other.mExtendedInfo; mResolvedIntent = other.mResolvedIntent; - mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter; + mPresentationGetter = other.mPresentationGetter; mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon()); } @@ -143,9 +143,9 @@ public class DisplayResolveInfo implements TargetInfo { } public CharSequence getDisplayLabel() { - if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) { - mDisplayLabel = mResolveInfoPresentationGetter.getLabel(); - mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel(); + if (mDisplayLabel == null && mPresentationGetter != null) { + mDisplayLabel = mPresentationGetter.getLabel(); + mExtendedInfo = mPresentationGetter.getSubLabel(); } return mDisplayLabel; } @@ -169,7 +169,7 @@ public class DisplayResolveInfo implements TargetInfo { @Override public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter); + return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter); } @Override diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 9f1dab77..97de97f5 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -38,7 +38,6 @@ import android.util.Size; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; @@ -250,7 +249,7 @@ public class ChooserWrapperActivity @Override public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { + @Nullable TargetPresentationGetter resolveInfoPresentationGetter) { return DisplayResolveInfo.newDisplayResolveInfo( originalIntent, pri, diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java index 0d44e147..af897a47 100644 --- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java +++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java @@ -22,7 +22,6 @@ import android.content.Intent; import android.content.pm.ResolveInfo; import android.os.UserHandle; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.intentresolver.chooser.DisplayResolveInfo; import java.util.concurrent.Executor; @@ -40,7 +39,7 @@ public interface IChooserWrapper { UsageStatsManager getUsageStatsManager(); DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter); + @Nullable TargetPresentationGetter resolveInfoPresentationGetter); UserHandle getCurrentUserHandle(); ChooserActivityLogger getChooserActivityLogger(); Executor getMainExecutor(); diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index 07cbd6a4..62c16ff5 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -27,7 +27,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.android.intentresolver.MatcherUtils.first; -import static com.android.intentresolver.ResolverDataProvider.createPackageManagerMockedInfo; import static com.android.intentresolver.ResolverWrapperActivity.sOverrides; import static org.hamcrest.CoreMatchers.allOf; @@ -39,34 +38,25 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; import static org.testng.Assert.fail; -import android.content.Context; import android.content.Intent; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Bundle; -import android.os.Parcelable; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; -import android.util.Log; import android.view.View; import android.widget.RelativeLayout; import android.widget.TextView; import androidx.test.InstrumentationRegistry; -import androidx.test.core.app.ActivityScenario; import androidx.test.espresso.Espresso; import androidx.test.espresso.NoMatchingViewException; -import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; -import com.android.internal.R; import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; -import com.android.intentresolver.ResolverDataProvider.PackageManagerMockedInfo; -import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.intentresolver.widget.ResolverDrawerLayout; +import com.android.internal.R; import org.junit.Before; import org.junit.Ignore; @@ -364,50 +354,6 @@ public class ResolverActivityTest { } @Test - public void getActivityLabelAndSubLabel() throws Exception { - ActivityInfoPresentationGetter pg; - PackageManagerMockedInfo info; - - info = createPackageManagerMockedInfo(false); - pg = new ActivityInfoPresentationGetter( - info.ctx, 0, info.activityInfo); - assertThat("Label should match app label", pg.getLabel().equals( - info.setAppLabel)); - assertThat("Sublabel should match activity label if set", - pg.getSubLabel().equals(info.setActivityLabel)); - - info = createPackageManagerMockedInfo(true); - pg = new ActivityInfoPresentationGetter( - info.ctx, 0, info.activityInfo); - assertThat("With override permission label should match activity label if set", - pg.getLabel().equals(info.setActivityLabel)); - assertThat("With override permission sublabel should be empty", - TextUtils.isEmpty(pg.getSubLabel())); - } - - @Test - public void getResolveInfoLabelAndSubLabel() throws Exception { - ResolveInfoPresentationGetter pg; - PackageManagerMockedInfo info; - - info = createPackageManagerMockedInfo(false); - pg = new ResolveInfoPresentationGetter( - info.ctx, 0, info.resolveInfo); - assertThat("Label should match app label", pg.getLabel().equals( - info.setAppLabel)); - assertThat("Sublabel should match resolve info label if set", - pg.getSubLabel().equals(info.setResolveInfoLabel)); - - info = createPackageManagerMockedInfo(true); - pg = new ResolveInfoPresentationGetter( - info.ctx, 0, info.resolveInfo); - assertThat("With override permission label should match activity label if set", - pg.getLabel().equals(info.setActivityLabel)); - assertThat("With override permission the sublabel should be the resolve info label", - pg.getSubLabel().equals(info.setResolveInfoLabel)); - } - - @Test public void testWorkTab_displayedWhenWorkProfileUserAvailable() { Intent sendIntent = createSendImageIntent(); markWorkProfileUserAvailable(); diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index 01d07639..fb928e09 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -93,11 +93,17 @@ public class ResolverDataProvider { public String setResolveInfoLabel; } + /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */ static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) { - final String appLabel = "app_label"; - final String activityLabel = "activity_label"; - final String resolveInfoLabel = "resolve_info_label"; + return createPackageManagerMockedInfo( + hasOverridePermission, "app_label", "activity_label", "resolve_info_label"); + } + static PackageManagerMockedInfo createPackageManagerMockedInfo( + boolean hasOverridePermission, + String appLabel, + String activityLabel, + String resolveInfoLabel) { MockContext ctx = new MockContext() { @Override public PackageManager getPackageManager() { diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt new file mode 100644 index 00000000..e62672a3 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import com.android.intentresolver.ResolverDataProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Unit tests for the various implementations of {@link TargetPresentationGetter}. + * TODO: consider expanding to cover icon logic (not just labels/sublabels). + * TODO: these are conceptually "acceptance tests" that provide comprehensive coverage of the + * apparent variations in the legacy implementation. The tests probably don't have to be so + * exhaustive if we're able to impose a simpler design on the implementation. + */ +class TargetPresentationGetterTest { + fun makeResolveInfoPresentationGetter( + withSubstitutePermission: Boolean, + appLabel: String, + activityLabel: String, + resolveInfoLabel: String): TargetPresentationGetter { + val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( + withSubstitutePermission, appLabel, activityLabel, resolveInfoLabel) + val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) + return factory.makePresentationGetter(testPackageInfo.resolveInfo) + } + + fun makeActivityInfoPresentationGetter( + withSubstitutePermission: Boolean, + appLabel: String?, + activityLabel: String?): TargetPresentationGetter { + val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( + withSubstitutePermission, appLabel, activityLabel, "") + val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) + return factory.makePresentationGetter(testPackageInfo.activityInfo) + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + false, "app_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + false, "app_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, there's no logic to dedupe the labels. + // TODO: this matches our observations in the legacy code, but is it the right behavior? It + // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in + // the UI at least, but maybe that logic should be pulled back to the "presentation"? + assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_nullRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, null, "activity_label") + assertThat(presentationGetter.getLabel()).isNull() + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, "", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("") + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, "app_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, empty sublabels are passed through as-is. + assertThat(presentationGetter.getSubLabel()).isEqualTo("") + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + true, "app_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, the same ("activity") label is requested as both the label + // and sublabel, even though the other value ("app_label") was distinct. Thus this behaves the + // same as a dupe. + assertThat(presentationGetter.getSubLabel()).isEqualTo(null) + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + true, "app_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // With the substitute permission, duped sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_nullRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", null) + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // With the substitute permission, null inputs are a special case that produces null outputs + // (i.e., they're not simply passed-through from the inputs). + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", "") + // Empty "labels" are taken as-is and (unlike nulls) don't prompt a fallback to the sublabel. + // Thus (as in the previous case with substitute permission & "distinct" labels), this is + // treated as a dupe. + assertThat(presentationGetter.getLabel()).isEqualTo("") + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "resolve_info_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, there's no logic to dedupe the labels. + // TODO: this matches our observations in the legacy code, but is it the right behavior? It + // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in + // the UI at least, but maybe that logic should be pulled back to the "presentation"? + assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, empty sublabels are passed through as-is. + assertThat(presentationGetter.getSubLabel()).isEqualTo("") + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "resolve_info_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, duped sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "", "") + assertThat(presentationGetter.getLabel()).isEqualTo("") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } +} |