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