1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.content.pm.ActivityInfo; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ResolveInfo; 25 import android.content.res.Resources; 26 import android.graphics.Bitmap; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.graphics.drawable.Drawable; 29 import android.os.UserHandle; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 /** 34 * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application icon 35 * and label over any IntentFilter or Activity icon to increase user understanding, with an 36 * exception for applications that hold the right permission. Always attempts to use available 37 * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses 38 * Strings to strip creative formatting. 39 * 40 * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the 41 * appropriate concrete type. 42 * 43 * TODO: once this component (and its tests) are merged, it should be possible to refactor and 44 * vastly simplify by precomputing conditional logic at initialization. 45 */ 46 public abstract class TargetPresentationGetter { 47 private static final String TAG = "ResolverListAdapter"; 48 49 /** Helper to build appropriate type-specific {@link TargetPresentationGetter} instances. */ 50 public static class Factory { 51 private final Context mContext; 52 private final int mIconDpi; 53 Factory(Context context, int iconDpi)54 public Factory(Context context, int iconDpi) { 55 mContext = context; 56 mIconDpi = iconDpi; 57 } 58 59 /** Make a {@link TargetPresentationGetter} for an {@link ActivityInfo}. */ makePresentationGetter(ActivityInfo activityInfo)60 public TargetPresentationGetter makePresentationGetter(ActivityInfo activityInfo) { 61 return new ActivityInfoPresentationGetter(mContext, mIconDpi, activityInfo); 62 } 63 64 /** Make a {@link TargetPresentationGetter} for a {@link ResolveInfo}. */ makePresentationGetter(ResolveInfo resolveInfo)65 public TargetPresentationGetter makePresentationGetter(ResolveInfo resolveInfo) { 66 return new ResolveInfoPresentationGetter(mContext, mIconDpi, resolveInfo); 67 } 68 } 69 70 @Nullable getIconSubstituteInternal()71 protected abstract Drawable getIconSubstituteInternal(); 72 73 @Nullable getAppSubLabelInternal()74 protected abstract String getAppSubLabelInternal(); 75 76 @Nullable getAppLabelForSubstitutePermission()77 protected abstract String getAppLabelForSubstitutePermission(); 78 79 private Context mContext; 80 private final int mIconDpi; 81 private final boolean mHasSubstitutePermission; 82 private final ApplicationInfo mAppInfo; 83 84 protected PackageManager mPm; 85 86 /** 87 * Retrieve the image that should be displayed as the icon when this target is presented to the 88 * specified {@code userHandle}. 89 */ getIcon(UserHandle userHandle)90 public Drawable getIcon(UserHandle userHandle) { 91 return new BitmapDrawable(mContext.getResources(), getIconBitmap(userHandle)); 92 } 93 94 /** 95 * Retrieve the image that should be displayed as the icon when this target is presented to the 96 * specified {@code userHandle}. 97 */ getIconBitmap(@ullable UserHandle userHandle)98 public Bitmap getIconBitmap(@Nullable UserHandle userHandle) { 99 Drawable drawable = null; 100 if (mHasSubstitutePermission) { 101 drawable = getIconSubstituteInternal(); 102 } 103 104 if (drawable == null) { 105 try { 106 if (mAppInfo.icon != 0) { 107 drawable = loadIconFromResource( 108 mPm.getResourcesForApplication(mAppInfo), mAppInfo.icon); 109 } 110 } catch (PackageManager.NameNotFoundException ignore) { } 111 } 112 113 // Fall back to ApplicationInfo#loadIcon if nothing has been loaded 114 if (drawable == null) { 115 drawable = mAppInfo.loadIcon(mPm); 116 } 117 118 SimpleIconFactory iconFactory = SimpleIconFactory.obtain(mContext); 119 Bitmap icon = iconFactory.createUserBadgedIconBitmap(drawable, userHandle); 120 iconFactory.recycle(); 121 122 return icon; 123 } 124 125 /** Get the label to display for the target. */ getLabel()126 public String getLabel() { 127 String label = null; 128 // Apps with the substitute permission will always show the activity label as the app label 129 // if provided. 130 if (mHasSubstitutePermission) { 131 label = getAppLabelForSubstitutePermission(); 132 } 133 134 if (label == null) { 135 label = (String) mAppInfo.loadLabel(mPm); 136 } 137 138 return label; 139 } 140 141 /** 142 * Get the sublabel to display for the target. Clients are responsible for deduping their 143 * presentation if this returns the same value as {@link #getLabel()}. 144 * TODO: this class should take responsibility for that deduping internally so it's an 145 * authoritative record of exactly the content that should be presented. 146 */ getSubLabel()147 public String getSubLabel() { 148 // Apps with the substitute permission will always show the resolve info label as the 149 // sublabel if provided 150 if (mHasSubstitutePermission) { 151 String appSubLabel = getAppSubLabelInternal(); 152 // Use the resolve info label as sublabel if it is set 153 if (!TextUtils.isEmpty(appSubLabel) && !TextUtils.equals(appSubLabel, getLabel())) { 154 return appSubLabel; 155 } 156 return null; 157 } 158 return getAppSubLabelInternal(); 159 } 160 loadLabelFromResource(Resources res, int resId)161 protected String loadLabelFromResource(Resources res, int resId) { 162 return res.getString(resId); 163 } 164 165 @Nullable loadIconFromResource(Resources res, int resId)166 protected Drawable loadIconFromResource(Resources res, int resId) { 167 return res.getDrawableForDensity(resId, mIconDpi); 168 } 169 TargetPresentationGetter(Context context, int iconDpi, ApplicationInfo appInfo)170 private TargetPresentationGetter(Context context, int iconDpi, ApplicationInfo appInfo) { 171 mContext = context; 172 mPm = context.getPackageManager(); 173 mAppInfo = appInfo; 174 mIconDpi = iconDpi; 175 mHasSubstitutePermission = (PackageManager.PERMISSION_GRANTED == mPm.checkPermission( 176 android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, 177 mAppInfo.packageName)); 178 } 179 180 /** Loads the icon and label for the provided ResolveInfo. */ 181 private static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { 182 private final ResolveInfo mResolveInfo; 183 ResolveInfoPresentationGetter( Context context, int iconDpi, ResolveInfo resolveInfo)184 ResolveInfoPresentationGetter( 185 Context context, int iconDpi, ResolveInfo resolveInfo) { 186 super(context, iconDpi, resolveInfo.activityInfo); 187 mResolveInfo = resolveInfo; 188 } 189 190 @Override getIconSubstituteInternal()191 protected Drawable getIconSubstituteInternal() { 192 Drawable drawable = null; 193 try { 194 // Do not use ResolveInfo#getIconResource() as it defaults to the app 195 if (mResolveInfo.resolvePackageName != null && mResolveInfo.icon != 0) { 196 drawable = loadIconFromResource( 197 mPm.getResourcesForApplication(mResolveInfo.resolvePackageName), 198 mResolveInfo.icon); 199 } 200 } catch (PackageManager.NameNotFoundException e) { 201 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 202 + "couldn't find resources for package", e); 203 } 204 205 // Fall back to ActivityInfo if no icon is found via ResolveInfo 206 if (drawable == null) { 207 drawable = super.getIconSubstituteInternal(); 208 } 209 210 return drawable; 211 } 212 213 @Override getAppSubLabelInternal()214 protected String getAppSubLabelInternal() { 215 // Will default to app name if no intent filter or activity label set, make sure to 216 // check if subLabel matches label before final display 217 return mResolveInfo.loadLabel(mPm).toString(); 218 } 219 220 @Override getAppLabelForSubstitutePermission()221 protected String getAppLabelForSubstitutePermission() { 222 // Will default to app name if no activity label set 223 return mResolveInfo.getComponentInfo().loadLabel(mPm).toString(); 224 } 225 } 226 227 /** Loads the icon and label for the provided {@link ActivityInfo}. */ 228 private static class ActivityInfoPresentationGetter extends TargetPresentationGetter { 229 private final ActivityInfo mActivityInfo; 230 ActivityInfoPresentationGetter( Context context, int iconDpi, ActivityInfo activityInfo)231 ActivityInfoPresentationGetter( 232 Context context, int iconDpi, ActivityInfo activityInfo) { 233 super(context, iconDpi, activityInfo.applicationInfo); 234 mActivityInfo = activityInfo; 235 } 236 237 @Override getIconSubstituteInternal()238 protected Drawable getIconSubstituteInternal() { 239 Drawable drawable = null; 240 try { 241 // Do not use ActivityInfo#getIconResource() as it defaults to the app 242 if (mActivityInfo.icon != 0) { 243 drawable = loadIconFromResource( 244 mPm.getResourcesForApplication(mActivityInfo.applicationInfo), 245 mActivityInfo.icon); 246 } 247 } catch (PackageManager.NameNotFoundException e) { 248 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 249 + "couldn't find resources for package", e); 250 } 251 252 return drawable; 253 } 254 255 @Override getAppSubLabelInternal()256 protected String getAppSubLabelInternal() { 257 // Will default to app name if no activity label set, make sure to check if subLabel 258 // matches label before final display 259 return (String) mActivityInfo.loadLabel(mPm); 260 } 261 262 @Override getAppLabelForSubstitutePermission()263 protected String getAppLabelForSubstitutePermission() { 264 return getAppSubLabelInternal(); 265 } 266 } 267 } 268