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