1 /*
2  * Copyright (C) 2019 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.internal.app.chooser;
18 
19 import android.annotation.Nullable;
20 import android.app.Activity;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.ActivityInfo;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.LauncherApps;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ShortcutInfo;
30 import android.graphics.Bitmap;
31 import android.graphics.drawable.BitmapDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.graphics.drawable.Icon;
34 import android.os.Bundle;
35 import android.os.UserHandle;
36 import android.service.chooser.ChooserTarget;
37 import android.text.SpannableStringBuilder;
38 import android.util.Log;
39 
40 import com.android.internal.app.ChooserActivity;
41 import com.android.internal.app.ChooserFlags;
42 import com.android.internal.app.ResolverActivity;
43 import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter;
44 import com.android.internal.app.SimpleIconFactory;
45 
46 import java.util.ArrayList;
47 import java.util.List;
48 
49 /**
50  * Live target, currently selectable by the user.
51  * @see NotSelectableTargetInfo
52  */
53 public final class SelectableTargetInfo implements ChooserTargetInfo {
54     private static final String TAG = "SelectableTargetInfo";
55 
56     private final Context mContext;
57     private final DisplayResolveInfo mSourceInfo;
58     private final ResolveInfo mBackupResolveInfo;
59     private final ChooserTarget mChooserTarget;
60     private final String mDisplayLabel;
61     private final PackageManager mPm;
62     private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
63     private Drawable mBadgeIcon = null;
64     private CharSequence mBadgeContentDescription;
65     private Drawable mDisplayIcon;
66     private final Intent mFillInIntent;
67     private final int mFillInFlags;
68     private final float mModifiedScore;
69     private boolean mIsSuspended = false;
70 
SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator, @Nullable ShortcutInfo shortcutInfo)71     public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
72             ChooserTarget chooserTarget,
73             float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator,
74             @Nullable ShortcutInfo shortcutInfo) {
75         mContext = context;
76         mSourceInfo = sourceInfo;
77         mChooserTarget = chooserTarget;
78         mModifiedScore = modifiedScore;
79         mPm = mContext.getPackageManager();
80         mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
81         if (sourceInfo != null) {
82             final ResolveInfo ri = sourceInfo.getResolveInfo();
83             if (ri != null) {
84                 final ActivityInfo ai = ri.activityInfo;
85                 if (ai != null && ai.applicationInfo != null) {
86                     final PackageManager pm = mContext.getPackageManager();
87                     mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
88                     mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
89                     mIsSuspended =
90                             (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
91                 }
92             }
93         }
94         // TODO(b/121287224): do this in the background thread, and only for selected targets
95         mDisplayIcon = getChooserTargetIconDrawable(chooserTarget, shortcutInfo);
96 
97         if (sourceInfo != null) {
98             mBackupResolveInfo = null;
99         } else {
100             mBackupResolveInfo =
101                     mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
102         }
103 
104         mFillInIntent = null;
105         mFillInFlags = 0;
106 
107         mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
108     }
109 
SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags)110     private SelectableTargetInfo(SelectableTargetInfo other,
111             Intent fillInIntent, int flags) {
112         mContext = other.mContext;
113         mPm = other.mPm;
114         mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
115         mSourceInfo = other.mSourceInfo;
116         mBackupResolveInfo = other.mBackupResolveInfo;
117         mChooserTarget = other.mChooserTarget;
118         mBadgeIcon = other.mBadgeIcon;
119         mBadgeContentDescription = other.mBadgeContentDescription;
120         mDisplayIcon = other.mDisplayIcon;
121         mFillInIntent = fillInIntent;
122         mFillInFlags = flags;
123         mModifiedScore = other.mModifiedScore;
124 
125         mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
126     }
127 
sanitizeDisplayLabel(CharSequence label)128     private String sanitizeDisplayLabel(CharSequence label) {
129         SpannableStringBuilder sb = new SpannableStringBuilder(label);
130         sb.clearSpans();
131         return sb.toString();
132     }
133 
isSuspended()134     public boolean isSuspended() {
135         return mIsSuspended;
136     }
137 
getDisplayResolveInfo()138     public DisplayResolveInfo getDisplayResolveInfo() {
139         return mSourceInfo;
140     }
141 
getChooserTargetIconDrawable(ChooserTarget target, @Nullable ShortcutInfo shortcutInfo)142     private Drawable getChooserTargetIconDrawable(ChooserTarget target,
143             @Nullable ShortcutInfo shortcutInfo) {
144         Drawable directShareIcon = null;
145 
146         // First get the target drawable and associated activity info
147         final Icon icon = target.getIcon();
148         if (icon != null) {
149             directShareIcon = icon.loadDrawable(mContext);
150         } else if (ChooserFlags.USE_SHORTCUT_MANAGER_FOR_DIRECT_TARGETS && shortcutInfo != null) {
151             LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
152                     Context.LAUNCHER_APPS_SERVICE);
153             directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
154         }
155 
156         if (directShareIcon == null) return null;
157 
158         ActivityInfo info = null;
159         try {
160             info = mPm.getActivityInfo(target.getComponentName(), 0);
161         } catch (PackageManager.NameNotFoundException error) {
162             Log.e(TAG, "Could not find activity associated with ChooserTarget");
163         }
164 
165         if (info == null) return null;
166 
167         // Now fetch app icon and raster with no badging even in work profile
168         Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
169                 .getIconBitmap(UserHandle.getUserHandleForUid(UserHandle.myUserId()));
170 
171         // Raster target drawable with appIcon as a badge
172         SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
173         Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
174         sif.recycle();
175 
176         return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
177     }
178 
getModifiedScore()179     public float getModifiedScore() {
180         return mModifiedScore;
181     }
182 
183     @Override
getResolvedIntent()184     public Intent getResolvedIntent() {
185         if (mSourceInfo != null) {
186             return mSourceInfo.getResolvedIntent();
187         }
188 
189         final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
190         targetIntent.setComponent(mChooserTarget.getComponentName());
191         targetIntent.putExtras(mChooserTarget.getIntentExtras());
192         return targetIntent;
193     }
194 
195     @Override
getResolvedComponentName()196     public ComponentName getResolvedComponentName() {
197         if (mSourceInfo != null) {
198             return mSourceInfo.getResolvedComponentName();
199         } else if (mBackupResolveInfo != null) {
200             return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
201                     mBackupResolveInfo.activityInfo.name);
202         }
203         return null;
204     }
205 
getBaseIntentToSend()206     private Intent getBaseIntentToSend() {
207         Intent result = getResolvedIntent();
208         if (result == null) {
209             Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
210         } else {
211             result = new Intent(result);
212             if (mFillInIntent != null) {
213                 result.fillIn(mFillInIntent, mFillInFlags);
214             }
215             result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
216         }
217         return result;
218     }
219 
220     @Override
start(Activity activity, Bundle options)221     public boolean start(Activity activity, Bundle options) {
222         throw new RuntimeException("ChooserTargets should be started as caller.");
223     }
224 
225     @Override
startAsCaller(ResolverActivity activity, Bundle options, int userId)226     public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
227         final Intent intent = getBaseIntentToSend();
228         if (intent == null) {
229             return false;
230         }
231         intent.setComponent(mChooserTarget.getComponentName());
232         intent.putExtras(mChooserTarget.getIntentExtras());
233 
234         // Important: we will ignore the target security checks in ActivityManager
235         // if and only if the ChooserTarget's target package is the same package
236         // where we got the ChooserTargetService that provided it. This lets a
237         // ChooserTargetService provide a non-exported or permission-guarded target
238         // to the chooser for the user to pick.
239         //
240         // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
241         // so we'll obey the caller's normal security checks.
242         final boolean ignoreTargetSecurity = mSourceInfo != null
243                 && mSourceInfo.getResolvedComponentName().getPackageName()
244                 .equals(mChooserTarget.getComponentName().getPackageName());
245         return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId);
246     }
247 
248     @Override
startAsUser(Activity activity, Bundle options, UserHandle user)249     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
250         throw new RuntimeException("ChooserTargets should be started as caller.");
251     }
252 
253     @Override
getResolveInfo()254     public ResolveInfo getResolveInfo() {
255         return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
256     }
257 
258     @Override
getDisplayLabel()259     public CharSequence getDisplayLabel() {
260         return mDisplayLabel;
261     }
262 
263     @Override
getExtendedInfo()264     public CharSequence getExtendedInfo() {
265         // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
266         return null;
267     }
268 
269     @Override
getDisplayIcon(Context context)270     public Drawable getDisplayIcon(Context context) {
271         return mDisplayIcon;
272     }
273 
getChooserTarget()274     public ChooserTarget getChooserTarget() {
275         return mChooserTarget;
276     }
277 
278     @Override
cloneFilledIn(Intent fillInIntent, int flags)279     public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
280         return new SelectableTargetInfo(this, fillInIntent, flags);
281     }
282 
283     @Override
getAllSourceIntents()284     public List<Intent> getAllSourceIntents() {
285         final List<Intent> results = new ArrayList<>();
286         if (mSourceInfo != null) {
287             // We only queried the service for the first one in our sourceinfo.
288             results.add(mSourceInfo.getAllSourceIntents().get(0));
289         }
290         return results;
291     }
292 
293     @Override
isPinned()294     public boolean isPinned() {
295         return mSourceInfo != null && mSourceInfo.isPinned();
296     }
297 
298     /**
299      * Necessary methods to communicate between {@link SelectableTargetInfo}
300      * and {@link ResolverActivity} or {@link ChooserActivity}.
301      */
302     public interface SelectableTargetInfoCommunicator {
303 
makePresentationGetter(ActivityInfo info)304         ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
305 
getTargetIntent()306         Intent getTargetIntent();
307 
getReferrerFillInIntent()308         Intent getReferrerFillInIntent();
309     }
310 }
311