/* * 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.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; import androidx.annotation.Nullable; /** * 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(); } } }