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.pm.ActivityInfo; 20 import android.content.pm.ApplicationInfo; 21 import android.content.pm.PackageManager; 22 import android.content.pm.ResolveInfo; 23 import android.content.res.Resources; 24 import android.graphics.Bitmap; 25 import android.graphics.drawable.Drawable; 26 import android.os.UserHandle; 27 import android.text.TextUtils; 28 import android.util.Log; 29 30 import androidx.annotation.Nullable; 31 32 import javax.inject.Provider; 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 Provider<SimpleIconFactory> mIconFactoryProvider; 53 private final PackageManager mPackageManager; 54 private final int mIconDpi; 55 Factory( Provider<SimpleIconFactory> iconfactoryProvider, PackageManager packageManager, int iconDpi)56 public Factory( 57 Provider<SimpleIconFactory> iconfactoryProvider, 58 PackageManager packageManager, 59 int iconDpi) { 60 mIconFactoryProvider = iconfactoryProvider; 61 mPackageManager = packageManager; 62 mIconDpi = iconDpi; 63 } 64 65 /** Make a {@link TargetPresentationGetter} for an {@link ActivityInfo}. */ makePresentationGetter(ActivityInfo activityInfo)66 public TargetPresentationGetter makePresentationGetter(ActivityInfo activityInfo) { 67 return new ActivityInfoPresentationGetter( 68 mIconFactoryProvider, mPackageManager, mIconDpi, activityInfo); 69 } 70 71 /** Make a {@link TargetPresentationGetter} for a {@link ResolveInfo}. */ makePresentationGetter(ResolveInfo resolveInfo)72 public TargetPresentationGetter makePresentationGetter(ResolveInfo resolveInfo) { 73 return new ResolveInfoPresentationGetter( 74 mIconFactoryProvider, mPackageManager, mIconDpi, resolveInfo); 75 } 76 } 77 78 @Nullable getIconSubstituteInternal()79 protected abstract Drawable getIconSubstituteInternal(); 80 81 @Nullable getAppSubLabelInternal()82 protected abstract String getAppSubLabelInternal(); 83 84 @Nullable getAppLabelForSubstitutePermission()85 protected abstract String getAppLabelForSubstitutePermission(); 86 87 private final Provider<SimpleIconFactory> mIconFactoryProvider; 88 private final int mIconDpi; 89 private final boolean mHasSubstitutePermission; 90 private final ApplicationInfo mAppInfo; 91 92 protected PackageManager mPm; 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 Bitmap icon; 119 try (SimpleIconFactory iconFactory = mIconFactoryProvider.get()) { 120 icon = iconFactory.createUserBadgedIconBitmap(drawable, userHandle); 121 } 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( Provider<SimpleIconFactory> iconfactoryProvider, PackageManager packageManager, int iconDpi, ApplicationInfo appInfo)171 private TargetPresentationGetter( 172 Provider<SimpleIconFactory> iconfactoryProvider, 173 PackageManager packageManager, 174 int iconDpi, 175 ApplicationInfo appInfo) { 176 mIconFactoryProvider = iconfactoryProvider; 177 mPm = packageManager; 178 mAppInfo = appInfo; 179 mIconDpi = iconDpi; 180 mHasSubstitutePermission = (PackageManager.PERMISSION_GRANTED == mPm.checkPermission( 181 android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, 182 mAppInfo.packageName)); 183 } 184 185 /** Loads the icon and label for the provided ResolveInfo. */ 186 private static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { 187 private final ResolveInfo mResolveInfo; 188 ResolveInfoPresentationGetter( Provider<SimpleIconFactory> iconfactoryProvider, PackageManager packageManager, int iconDpi, ResolveInfo resolveInfo)189 ResolveInfoPresentationGetter( 190 Provider<SimpleIconFactory> iconfactoryProvider, 191 PackageManager packageManager, 192 int iconDpi, 193 ResolveInfo resolveInfo) { 194 super(iconfactoryProvider, packageManager, iconDpi, resolveInfo.activityInfo); 195 mResolveInfo = resolveInfo; 196 } 197 198 @Override getIconSubstituteInternal()199 protected Drawable getIconSubstituteInternal() { 200 Drawable drawable = null; 201 try { 202 // Do not use ResolveInfo#getIconResource() as it defaults to the app 203 if (mResolveInfo.resolvePackageName != null && mResolveInfo.icon != 0) { 204 drawable = loadIconFromResource( 205 mPm.getResourcesForApplication(mResolveInfo.resolvePackageName), 206 mResolveInfo.icon); 207 } 208 } catch (PackageManager.NameNotFoundException e) { 209 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 210 + "couldn't find resources for package", e); 211 } 212 213 // Fall back to ActivityInfo if no icon is found via ResolveInfo 214 if (drawable == null) { 215 drawable = super.getIconSubstituteInternal(); 216 } 217 218 return drawable; 219 } 220 221 @Override getAppSubLabelInternal()222 protected String getAppSubLabelInternal() { 223 // Will default to app name if no intent filter or activity label set, make sure to 224 // check if subLabel matches label before final display 225 return mResolveInfo.loadLabel(mPm).toString(); 226 } 227 228 @Override getAppLabelForSubstitutePermission()229 protected String getAppLabelForSubstitutePermission() { 230 // Will default to app name if no activity label set 231 return mResolveInfo.getComponentInfo().loadLabel(mPm).toString(); 232 } 233 } 234 235 /** Loads the icon and label for the provided {@link ActivityInfo}. */ 236 private static class ActivityInfoPresentationGetter extends TargetPresentationGetter { 237 private final ActivityInfo mActivityInfo; 238 ActivityInfoPresentationGetter( Provider<SimpleIconFactory> iconfactoryProvider, PackageManager packageManager, int iconDpi, ActivityInfo activityInfo)239 ActivityInfoPresentationGetter( 240 Provider<SimpleIconFactory> iconfactoryProvider, 241 PackageManager packageManager, 242 int iconDpi, 243 ActivityInfo activityInfo) { 244 super(iconfactoryProvider, packageManager, iconDpi, activityInfo.applicationInfo); 245 mActivityInfo = activityInfo; 246 } 247 248 @Override getIconSubstituteInternal()249 protected Drawable getIconSubstituteInternal() { 250 Drawable drawable = null; 251 try { 252 // Do not use ActivityInfo#getIconResource() as it defaults to the app 253 if (mActivityInfo.icon != 0) { 254 drawable = loadIconFromResource( 255 mPm.getResourcesForApplication(mActivityInfo.applicationInfo), 256 mActivityInfo.icon); 257 } 258 } catch (PackageManager.NameNotFoundException e) { 259 Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " 260 + "couldn't find resources for package", e); 261 } 262 263 return drawable; 264 } 265 266 @Override getAppSubLabelInternal()267 protected String getAppSubLabelInternal() { 268 // Will default to app name if no activity label set, make sure to check if subLabel 269 // matches label before final display 270 return (String) mActivityInfo.loadLabel(mPm); 271 } 272 273 @Override getAppLabelForSubstitutePermission()274 protected String getAppLabelForSubstitutePermission() { 275 return getAppSubLabelInternal(); 276 } 277 } 278 } 279