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.intentresolver.chooser;
18 
19 import android.app.Activity;
20 import android.app.prediction.AppTarget;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.ResolveInfo;
25 import android.content.pm.ShortcutInfo;
26 import android.graphics.drawable.Icon;
27 import android.os.Bundle;
28 import android.os.UserHandle;
29 import android.provider.DeviceConfig;
30 import android.service.chooser.ChooserTarget;
31 import android.text.SpannableStringBuilder;
32 import android.util.HashedStringCache;
33 import android.util.Log;
34 
35 import androidx.annotation.Nullable;
36 
37 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Live target, currently selectable by the user.
44  * @see NotSelectableTargetInfo
45  */
46 public final class SelectableTargetInfo extends ChooserTargetInfo {
47     private static final String TAG = "SelectableTargetInfo";
48 
49     private interface TargetHashProvider {
getHashedTargetIdForMetrics(Context context)50         HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context);
51     }
52 
53     private interface TargetActivityStarter {
start(Activity activity, Bundle options)54         boolean start(Activity activity, Bundle options);
startAsCaller(Activity activity, Bundle options, int userId)55         boolean startAsCaller(Activity activity, Bundle options, int userId);
startAsUser(Activity activity, Bundle options, UserHandle user)56         boolean startAsUser(Activity activity, Bundle options, UserHandle user);
57     }
58 
59     private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity";  // For legacy reasons.
60     private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
61 
62     private final int mMaxHashSaltDays = DeviceConfig.getInt(
63             DeviceConfig.NAMESPACE_SYSTEMUI,
64             SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
65             DEFAULT_SALT_EXPIRATION_DAYS);
66 
67     @Nullable
68     private final DisplayResolveInfo mSourceInfo;
69     @Nullable
70     private final ResolveInfo mBackupResolveInfo;
71     private final Intent mResolvedIntent;
72     private final String mDisplayLabel;
73     @Nullable
74     private final AppTarget mAppTarget;
75     @Nullable
76     private final ShortcutInfo mShortcutInfo;
77 
78     private final ComponentName mChooserTargetComponentName;
79     private final CharSequence mChooserTargetUnsanitizedTitle;
80     private final Icon mChooserTargetIcon;
81     private final Bundle mChooserTargetIntentExtras;
82     private final boolean mIsPinned;
83     private final float mModifiedScore;
84     private final boolean mIsSuspended;
85     private final ComponentName mResolvedComponentName;
86     private final Intent mBaseIntentToSend;
87     private final ResolveInfo mResolveInfo;
88     private final List<Intent> mAllSourceIntents;
89     private final IconHolder mDisplayIconHolder = new SettableIconHolder();
90     private final TargetHashProvider mHashProvider;
91     private final TargetActivityStarter mActivityStarter;
92 
93     /**
94      * An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null})
95      * in its extended data under the key {@link Intent#EXTRA_REFERRER}.
96      */
97     private final Intent mReferrerFillInIntent;
98 
99     /**
100      * Create a new {@link TargetInfo} instance representing a selectable target. Some target
101      * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure.
102      *
103      * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}.
104      */
105     @Deprecated
newSelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, ChooserTarget chooserTarget, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)106     public static TargetInfo newSelectableTargetInfo(
107             @Nullable DisplayResolveInfo sourceInfo,
108             @Nullable ResolveInfo backupResolveInfo,
109             Intent resolvedIntent,
110             ChooserTarget chooserTarget,
111             float modifiedScore,
112             @Nullable ShortcutInfo shortcutInfo,
113             @Nullable AppTarget appTarget,
114             Intent referrerFillInIntent) {
115         return newSelectableTargetInfo(
116                 sourceInfo,
117                 backupResolveInfo,
118                 resolvedIntent,
119                 chooserTarget.getComponentName(),
120                 chooserTarget.getTitle(),
121                 chooserTarget.getIcon(),
122                 chooserTarget.getIntentExtras(),
123                 modifiedScore,
124                 shortcutInfo,
125                 appTarget,
126                 referrerFillInIntent);
127     }
128 
129     /**
130      * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*`
131      * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures
132      * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed
133      * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly
134      * as if they had been provided in the legacy representation.
135      *
136      * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename
137      * to avoid making reference to the legacy type; and reflect the improved semantics in the
138      * signature (and documentation) of this method.
139      */
newSelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, ComponentName chooserTargetComponentName, CharSequence chooserTargetUnsanitizedTitle, Icon chooserTargetIcon, @Nullable Bundle chooserTargetIntentExtras, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)140     public static TargetInfo newSelectableTargetInfo(
141             @Nullable DisplayResolveInfo sourceInfo,
142             @Nullable ResolveInfo backupResolveInfo,
143             Intent resolvedIntent,
144             ComponentName chooserTargetComponentName,
145             CharSequence chooserTargetUnsanitizedTitle,
146             Icon chooserTargetIcon,
147             @Nullable Bundle chooserTargetIntentExtras,
148             float modifiedScore,
149             @Nullable ShortcutInfo shortcutInfo,
150             @Nullable AppTarget appTarget,
151             Intent referrerFillInIntent) {
152         return new SelectableTargetInfo(
153                 sourceInfo,
154                 backupResolveInfo,
155                 resolvedIntent,
156                 null,
157                 chooserTargetComponentName,
158                 chooserTargetUnsanitizedTitle,
159                 chooserTargetIcon,
160                 chooserTargetIntentExtras,
161                 modifiedScore,
162                 shortcutInfo,
163                 appTarget,
164                 referrerFillInIntent);
165     }
166 
SelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, @Nullable Intent baseIntentToSend, ComponentName chooserTargetComponentName, CharSequence chooserTargetUnsanitizedTitle, Icon chooserTargetIcon, Bundle chooserTargetIntentExtras, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)167     private SelectableTargetInfo(
168             @Nullable DisplayResolveInfo sourceInfo,
169             @Nullable ResolveInfo backupResolveInfo,
170             Intent resolvedIntent,
171             @Nullable Intent baseIntentToSend,
172             ComponentName chooserTargetComponentName,
173             CharSequence chooserTargetUnsanitizedTitle,
174             Icon chooserTargetIcon,
175             Bundle chooserTargetIntentExtras,
176             float modifiedScore,
177             @Nullable ShortcutInfo shortcutInfo,
178             @Nullable AppTarget appTarget,
179             Intent referrerFillInIntent) {
180         mSourceInfo = sourceInfo;
181         mBackupResolveInfo = backupResolveInfo;
182         mResolvedIntent = resolvedIntent;
183         mModifiedScore = modifiedScore;
184         mShortcutInfo = shortcutInfo;
185         mAppTarget = appTarget;
186         mReferrerFillInIntent = referrerFillInIntent;
187         mChooserTargetComponentName = chooserTargetComponentName;
188         mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
189         mChooserTargetIcon = chooserTargetIcon;
190         mChooserTargetIntentExtras = chooserTargetIntentExtras;
191 
192         mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned();
193         mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
194         mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended();
195         mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
196 
197         mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo);
198 
199         mBaseIntentToSend = getBaseIntentToSend(
200                 baseIntentToSend,
201                 mResolvedIntent,
202                 mReferrerFillInIntent);
203 
204         mAllSourceIntents = getAllSourceIntents(sourceInfo, mBaseIntentToSend);
205 
206         mHashProvider = context -> {
207             final String plaintext =
208                     getChooserTargetComponentName().getPackageName()
209                     + mChooserTargetUnsanitizedTitle;
210             return HashedStringCache.getInstance().hashString(
211                     context,
212                     HASHED_STRING_CACHE_TAG,
213                     plaintext,
214                     mMaxHashSaltDays);
215         };
216 
217         mActivityStarter = new TargetActivityStarter() {
218             @Override
219             public boolean start(Activity activity, Bundle options) {
220                 throw new RuntimeException("ChooserTargets should be started as caller.");
221             }
222 
223             @Override
224             public boolean startAsCaller(Activity activity, Bundle options, int userId) {
225                 final Intent intent = mBaseIntentToSend;
226                 if (intent == null) {
227                     return false;
228                 }
229                 intent.setComponent(getChooserTargetComponentName());
230                 intent.putExtras(mChooserTargetIntentExtras);
231                 TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId);
232 
233                 // Important: we will ignore the target security checks in ActivityManager if and
234                 // only if the ChooserTarget's target package is the same package where we got the
235                 // ChooserTargetService that provided it. This lets a ChooserTargetService provide
236                 // a non-exported or permission-guarded target for the user to pick.
237                 //
238                 // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
239                 // so we'll obey the caller's normal security checks.
240                 final boolean ignoreTargetSecurity = (mSourceInfo != null)
241                         && mSourceInfo.getResolvedComponentName().getPackageName()
242                                 .equals(getChooserTargetComponentName().getPackageName());
243                 activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
244                 return true;
245             }
246 
247             @Override
248             public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
249                 throw new RuntimeException("ChooserTargets should be started as caller.");
250             }
251         };
252     }
253 
SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend)254     private SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend) {
255         this(
256                 other.mSourceInfo,
257                 other.mBackupResolveInfo,
258                 other.mResolvedIntent,
259                 baseIntentToSend,
260                 other.mChooserTargetComponentName,
261                 other.mChooserTargetUnsanitizedTitle,
262                 other.mChooserTargetIcon,
263                 other.mChooserTargetIntentExtras,
264                 other.mModifiedScore,
265                 other.mShortcutInfo,
266                 other.mAppTarget,
267                 other.mReferrerFillInIntent);
268     }
269 
270     @Override
271     @Nullable
tryToCloneWithAppliedRefinement(Intent proposedRefinement)272     public TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
273         Intent matchingBase =
274                 getAllSourceIntents()
275                         .stream()
276                         .filter(i -> i.filterEquals(proposedRefinement))
277                         .findFirst()
278                         .orElse(null);
279         if (matchingBase == null) {
280             return null;
281         }
282 
283         return new SelectableTargetInfo(
284                 this,
285                 TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));
286     }
287 
288     @Override
getHashedTargetIdForMetrics(Context context)289     public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
290         return mHashProvider.getHashedTargetIdForMetrics(context);
291     }
292 
293     @Override
isSelectableTargetInfo()294     public boolean isSelectableTargetInfo() {
295         return true;
296     }
297 
298     @Override
isSuspended()299     public boolean isSuspended() {
300         return mIsSuspended;
301     }
302 
303     @Override
304     @Nullable
getDisplayResolveInfo()305     public DisplayResolveInfo getDisplayResolveInfo() {
306         return mSourceInfo;
307     }
308 
309     @Override
getModifiedScore()310     public float getModifiedScore() {
311         return mModifiedScore;
312     }
313 
314     @Override
getResolvedIntent()315     public Intent getResolvedIntent() {
316         return mResolvedIntent;
317     }
318 
319     @Override
getResolvedComponentName()320     public ComponentName getResolvedComponentName() {
321         return mResolvedComponentName;
322     }
323 
324     @Override
getChooserTargetComponentName()325     public ComponentName getChooserTargetComponentName() {
326         return mChooserTargetComponentName;
327     }
328 
329     @Nullable
getChooserTargetIcon()330     public Icon getChooserTargetIcon() {
331         return mChooserTargetIcon;
332     }
333 
334     @Override
startAsCaller(Activity activity, Bundle options, int userId)335     public boolean startAsCaller(Activity activity, Bundle options, int userId) {
336         return mActivityStarter.startAsCaller(activity, options, userId);
337     }
338 
339     @Override
startAsUser(Activity activity, Bundle options, UserHandle user)340     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
341         return mActivityStarter.startAsUser(activity, options, user);
342     }
343 
344     @Nullable
345     @Override
getTargetIntent()346     public Intent getTargetIntent() {
347         return mBaseIntentToSend;
348     }
349 
350     @Override
getResolveInfo()351     public ResolveInfo getResolveInfo() {
352         return mResolveInfo;
353     }
354 
355     @Override
getDisplayLabel()356     public CharSequence getDisplayLabel() {
357         return mDisplayLabel;
358     }
359 
360     @Override
getExtendedInfo()361     public CharSequence getExtendedInfo() {
362         // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
363         return null;
364     }
365 
366     @Override
getDisplayIconHolder()367     public IconHolder getDisplayIconHolder() {
368         return mDisplayIconHolder;
369     }
370 
371     @Override
372     @Nullable
getDirectShareShortcutInfo()373     public ShortcutInfo getDirectShareShortcutInfo() {
374         return mShortcutInfo;
375     }
376 
377     @Override
378     @Nullable
getDirectShareAppTarget()379     public AppTarget getDirectShareAppTarget() {
380         return mAppTarget;
381     }
382 
383     @Override
getAllSourceIntents()384     public List<Intent> getAllSourceIntents() {
385         return mAllSourceIntents;
386     }
387 
388     @Override
isPinned()389     public boolean isPinned() {
390         return mIsPinned;
391     }
392 
sanitizeDisplayLabel(CharSequence label)393     private static String sanitizeDisplayLabel(CharSequence label) {
394         SpannableStringBuilder sb = new SpannableStringBuilder(label);
395         sb.clearSpans();
396         return sb.toString();
397     }
398 
getAllSourceIntents( @ullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent)399     private static List<Intent> getAllSourceIntents(
400             @Nullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent) {
401         final List<Intent> results = new ArrayList<>();
402         if (sourceInfo != null) {
403             results.addAll(sourceInfo.getAllSourceIntents());
404         } else {
405             // This target wasn't joined to a `DisplayResolveInfo` result from our intent-resolution
406             // step, so it was provided directly by the caller. We don't support alternate intents
407             // in this case, but we still permit refinement of the intent we'll dispatch; e.g.,
408             // clients may use this hook to defer the computation of "lazy" extras in their share
409             // payload. Note this accommodation isn't strictly "necessary" because clients could
410             // always implement equivalent behavior by pointing custom targets back at their own app
411             // for any amount of further refinement/modification outside of the Sharesheet flow;
412             // nevertheless, it's offered as a convenience for clients who may expect their normal
413             // refinement logic to apply equally in the case of these "special targets."
414             results.add(fallbackSourceIntent);
415         }
416         return results;
417     }
418 
getResolvedComponentName( @ullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo)419     private static ComponentName getResolvedComponentName(
420             @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) {
421         if (sourceInfo != null) {
422             return sourceInfo.getResolvedComponentName();
423         } else if (backupResolveInfo != null) {
424             return new ComponentName(
425                     backupResolveInfo.activityInfo.packageName,
426                     backupResolveInfo.activityInfo.name);
427         }
428         return null;
429     }
430 
431     @Nullable
getBaseIntentToSend( @ullable Intent providedBase, @Nullable Intent fallbackBase, Intent referrerFillInIntent)432     private static Intent getBaseIntentToSend(
433             @Nullable Intent providedBase,
434             @Nullable Intent fallbackBase,
435             Intent referrerFillInIntent) {
436         Intent result = (providedBase != null) ? providedBase : fallbackBase;
437         if (result == null) {
438             Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
439         } else {
440             result = new Intent(result);
441             result.fillIn(referrerFillInIntent, 0);
442         }
443         return result;
444     }
445 }
446