| /* |
| * 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(); |
| } |
| } |
| } |