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