• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.IntentSender;
25 import android.net.Uri;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.os.PatternMatcher;
29 import android.service.chooser.ChooserAction;
30 import android.service.chooser.ChooserTarget;
31 import android.text.TextUtils;
32 import android.util.Log;
33 import android.util.Pair;
34 
35 import com.android.intentresolver.flags.FeatureFlagRepository;
36 import com.android.intentresolver.util.UriFilters;
37 
38 import com.google.common.collect.ImmutableList;
39 
40 import java.net.URISyntaxException;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.List;
44 import java.util.stream.Collector;
45 import java.util.stream.Collectors;
46 import java.util.stream.Stream;
47 
48 /**
49  * Utility to parse and validate parameters from the client-supplied {@link Intent} that launched
50  * the Sharesheet {@link ChooserActivity}. The validated parameters are stored as immutable ivars.
51  *
52  * TODO: field nullability in this class reflects legacy use, and typically would indicate that the
53  * client's intent didn't provide the respective data. In some cases we may be able to provide
54  * defaults instead of nulls -- especially for methods that return nullable lists or arrays, if the
55  * client code could instead handle empty collections equally well.
56  *
57  * TODO: some of these fields (especially getTargetIntent() and any other getters that delegate to
58  * it internally) differ from the legacy model because they're computed directly from the initial
59  * Chooser intent, where in the past they've been relayed up to ResolverActivity and then retrieved
60  * through methods on the base class. The base always seems to return them exactly as they were
61  * provided, so this should be safe -- and clients can reasonably switch to retrieving through these
62  * parameters instead. For now, the other convention is still used in some places. Ideally we'd like
63  * to normalize on a single source of truth, but we'll have to clean up the delegation up to the
64  * resolver (or perhaps this needs to be a subclass of some `ResolverRequestParameters` class?).
65  */
66 public class ChooserRequestParameters {
67     private static final String TAG = "ChooserActivity";
68 
69     private static final int LAUNCH_FLAGS_FOR_SEND_ACTION =
70             Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
71     private static final int MAX_CHOOSER_ACTIONS = 5;
72 
73     private final Intent mTarget;
74     private final String mReferrerPackageName;
75     private final Pair<CharSequence, Integer> mTitleSpec;
76     private final Intent mReferrerFillInIntent;
77     private final ImmutableList<ComponentName> mFilteredComponentNames;
78     private final ImmutableList<ChooserTarget> mCallerChooserTargets;
79     private final @NonNull ImmutableList<ChooserAction> mChooserActions;
80     private final ChooserAction mModifyShareAction;
81     private final boolean mRetainInOnStop;
82 
83     @Nullable
84     private final ImmutableList<Intent> mAdditionalTargets;
85 
86     @Nullable
87     private final Bundle mReplacementExtras;
88 
89     @Nullable
90     private final ImmutableList<Intent> mInitialIntents;
91 
92     @Nullable
93     private final IntentSender mChosenComponentSender;
94 
95     @Nullable
96     private final IntentSender mRefinementIntentSender;
97 
98     @Nullable
99     private final String mSharedText;
100 
101     @Nullable
102     private final IntentFilter mTargetIntentFilter;
103 
ChooserRequestParameters( final Intent clientIntent, String referrerPackageName, final Uri referrer, FeatureFlagRepository featureFlags)104     public ChooserRequestParameters(
105             final Intent clientIntent,
106             String referrerPackageName,
107             final Uri referrer,
108             FeatureFlagRepository featureFlags) {
109         final Intent requestedTarget = parseTargetIntentExtra(
110                 clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
111         mTarget = intentWithModifiedLaunchFlags(requestedTarget);
112 
113         mReferrerPackageName = referrerPackageName;
114 
115         mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
116                 clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
117 
118         mReplacementExtras = clientIntent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
119 
120         mTitleSpec = makeTitleSpec(
121                 clientIntent.getCharSequenceExtra(Intent.EXTRA_TITLE),
122                 isSendAction(mTarget.getAction()));
123 
124         mInitialIntents = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
125                 clientIntent, Intent.EXTRA_INITIAL_INTENTS);
126 
127         mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer);
128 
129         mChosenComponentSender = clientIntent.getParcelableExtra(
130                 Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
131         mRefinementIntentSender = clientIntent.getParcelableExtra(
132                 Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
133 
134         ComponentName[] filteredComponents = clientIntent.getParcelableArrayExtra(
135                 Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class);
136         mFilteredComponentNames = filteredComponents != null
137                 ? ImmutableList.copyOf(filteredComponents)
138                 : ImmutableList.of();
139 
140         mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
141 
142         mRetainInOnStop = clientIntent.getBooleanExtra(
143                 ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false);
144 
145         mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT);
146 
147         mTargetIntentFilter = getTargetIntentFilter(mTarget);
148 
149         mChooserActions = getChooserActions(clientIntent);
150         mModifyShareAction = getModifyShareAction(clientIntent);
151     }
152 
getTargetIntent()153     public Intent getTargetIntent() {
154         return mTarget;
155     }
156 
157     @Nullable
getTargetAction()158     public String getTargetAction() {
159         return getTargetIntent().getAction();
160     }
161 
isSendActionTarget()162     public boolean isSendActionTarget() {
163         return isSendAction(getTargetAction());
164     }
165 
166     @Nullable
getTargetType()167     public String getTargetType() {
168         return getTargetIntent().getType();
169     }
170 
getReferrerPackageName()171     public String getReferrerPackageName() {
172         return mReferrerPackageName;
173     }
174 
175     @Nullable
getTitle()176     public CharSequence getTitle() {
177         return mTitleSpec.first;
178     }
179 
getDefaultTitleResource()180     public int getDefaultTitleResource() {
181         return mTitleSpec.second;
182     }
183 
getReferrerFillInIntent()184     public Intent getReferrerFillInIntent() {
185         return mReferrerFillInIntent;
186     }
187 
getFilteredComponentNames()188     public ImmutableList<ComponentName> getFilteredComponentNames() {
189         return mFilteredComponentNames;
190     }
191 
getCallerChooserTargets()192     public ImmutableList<ChooserTarget> getCallerChooserTargets() {
193         return mCallerChooserTargets;
194     }
195 
196     @NonNull
getChooserActions()197     public ImmutableList<ChooserAction> getChooserActions() {
198         return mChooserActions;
199     }
200 
201     @Nullable
getModifyShareAction()202     public ChooserAction getModifyShareAction() {
203         return mModifyShareAction;
204     }
205 
206     /**
207      * Whether the {@link ChooserActivity#EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested.
208      */
shouldRetainInOnStop()209     public boolean shouldRetainInOnStop() {
210         return mRetainInOnStop;
211     }
212 
213     /**
214      * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
215      * refactored, returning {@link mAdditionalTargets} directly is simpler and safer.
216      */
217     @Nullable
getAdditionalTargets()218     public Intent[] getAdditionalTargets() {
219         return (mAdditionalTargets == null) ? null : mAdditionalTargets.toArray(new Intent[0]);
220     }
221 
222     @Nullable
getReplacementExtras()223     public Bundle getReplacementExtras() {
224         return mReplacementExtras;
225     }
226 
227     /**
228      * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
229      * refactored, returning {@link mInitialIntents} directly is simpler and safer.
230      */
231     @Nullable
getInitialIntents()232     public Intent[] getInitialIntents() {
233         return (mInitialIntents == null) ? null : mInitialIntents.toArray(new Intent[0]);
234     }
235 
236     @Nullable
getChosenComponentSender()237     public IntentSender getChosenComponentSender() {
238         return mChosenComponentSender;
239     }
240 
241     @Nullable
getRefinementIntentSender()242     public IntentSender getRefinementIntentSender() {
243         return mRefinementIntentSender;
244     }
245 
246     @Nullable
getSharedText()247     public String getSharedText() {
248         return mSharedText;
249     }
250 
251     @Nullable
getTargetIntentFilter()252     public IntentFilter getTargetIntentFilter() {
253         return mTargetIntentFilter;
254     }
255 
isSendAction(@ullable String action)256     private static boolean isSendAction(@Nullable String action) {
257         return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
258     }
259 
parseTargetIntentExtra(@ullable Parcelable targetParcelable)260     private static Intent parseTargetIntentExtra(@Nullable Parcelable targetParcelable) {
261         if (targetParcelable instanceof Uri) {
262             try {
263                 targetParcelable = Intent.parseUri(targetParcelable.toString(),
264                         Intent.URI_INTENT_SCHEME);
265             } catch (URISyntaxException ex) {
266                 throw new IllegalArgumentException("Failed to parse EXTRA_INTENT from URI", ex);
267             }
268         }
269 
270         if (!(targetParcelable instanceof Intent)) {
271             throw new IllegalArgumentException(
272                     "EXTRA_INTENT is neither an Intent nor a Uri: " + targetParcelable);
273         }
274 
275         return ((Intent) targetParcelable);
276     }
277 
intentWithModifiedLaunchFlags(Intent intent)278     private static Intent intentWithModifiedLaunchFlags(Intent intent) {
279         if (isSendAction(intent.getAction())) {
280             intent.addFlags(LAUNCH_FLAGS_FOR_SEND_ACTION);
281         }
282         return intent;
283     }
284 
285     /**
286      * Build a pair of values specifying the title to use from the client request. The first
287      * ({@link CharSequence}) value is the client-specified title, if there was one and their
288      * requested target <em>wasn't</em> a send action; otherwise it is null. The second value is
289      * the resource ID of a default title string; this is nonzero only if the first value is null.
290      *
291      * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or
292      * create a real type (not {@link Pair}) to express the semantics described in this comment.
293      */
makeTitleSpec( @ullable CharSequence requestedTitle, boolean hasSendActionTarget)294     private static Pair<CharSequence, Integer> makeTitleSpec(
295             @Nullable CharSequence requestedTitle, boolean hasSendActionTarget) {
296         if (hasSendActionTarget && (requestedTitle != null)) {
297             // Do not allow the title to be changed when sharing content
298             Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
299                     + " preview title by using EXTRA_TITLE property of the wrapped"
300                     + " EXTRA_INTENT.");
301             requestedTitle = null;
302         }
303 
304         int defaultTitleRes = (requestedTitle == null) ? R.string.chooseActivity : 0;
305 
306         return Pair.create(requestedTitle, defaultTitleRes);
307     }
308 
parseCallerTargetsFromClientIntent( Intent clientIntent)309     private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent(
310             Intent clientIntent) {
311         return
312                 streamParcelableArrayExtra(
313                         clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true)
314                 .collect(toImmutableList());
315     }
316 
317     @NonNull
getChooserActions(Intent intent)318     private static ImmutableList<ChooserAction> getChooserActions(Intent intent) {
319         return streamParcelableArrayExtra(
320                 intent,
321                 Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
322                 ChooserAction.class,
323                 true,
324                 true)
325                 .filter(UriFilters::hasValidIcon)
326                 .limit(MAX_CHOOSER_ACTIONS)
327                 .collect(toImmutableList());
328     }
329 
330     @Nullable
getModifyShareAction(Intent intent)331     private static ChooserAction getModifyShareAction(Intent intent) {
332         try {
333             return intent.getParcelableExtra(
334                     Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION,
335                     ChooserAction.class);
336         } catch (Throwable t) {
337             Log.w(
338                     TAG,
339                     "Unable to retrieve Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION argument",
340                     t);
341             return null;
342         }
343     }
344 
toImmutableList()345     private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() {
346         return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
347     }
348 
349     @Nullable
intentsWithModifiedLaunchFlagsFromExtraIfPresent( Intent clientIntent, String extra)350     private static ImmutableList<Intent> intentsWithModifiedLaunchFlagsFromExtraIfPresent(
351             Intent clientIntent, String extra) {
352         Stream<Intent> intents =
353                 streamParcelableArrayExtra(clientIntent, extra, Intent.class, true, false);
354         if (intents == null) {
355             return null;
356         }
357         return intents
358                 .map(ChooserRequestParameters::intentWithModifiedLaunchFlags)
359                 .collect(toImmutableList());
360     }
361 
362     /**
363      * Make a {@link Stream} of the {@link Parcelable} objects given in the provided {@link Intent}
364      * as the optional parcelable array extra with key {@code extra}. The stream elements, if any,
365      * are all of the type specified by {@code clazz}.
366      *
367      * @param intent The intent that may contain the optional extras.
368      * @param extra The extras key to identify the parcelable array.
369      * @param clazz A class that is assignable from any elements in the result stream.
370      * @param warnOnTypeError Whether to log a warning (and ignore) if the client extra doesn't have
371      * the required type. If false, throw an {@link IllegalArgumentException} if the extra is
372      * non-null but can't be assigned to variables of type {@code T}.
373      * @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't
374      * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true).
375      * If false, return null in these cases, and only return an empty stream if the intent
376      * explicitly provided an empty array for the specified extra.
377      */
378     @Nullable
streamParcelableArrayExtra( final Intent intent, String extra, @NonNull Class<T> clazz, boolean warnOnTypeError, boolean streamEmptyIfNull)379     private static <T extends Parcelable> Stream<T> streamParcelableArrayExtra(
380             final Intent intent,
381             String extra,
382             @NonNull Class<T> clazz,
383             boolean warnOnTypeError,
384             boolean streamEmptyIfNull) {
385         T[] result = null;
386 
387         try {
388             result = getParcelableArrayExtraIfPresent(intent, extra, clazz);
389         } catch (IllegalArgumentException e) {
390             if (warnOnTypeError) {
391                 Log.w(TAG, "Ignoring client-requested " + extra, e);
392             } else {
393                 throw e;
394             }
395         }
396 
397         if (result != null) {
398             return Arrays.stream(result);
399         } else if (streamEmptyIfNull) {
400             return Stream.empty();
401         } else {
402             return null;
403         }
404     }
405 
406     /**
407      * If the specified {@code extra} is provided in the {@code intent}, cast it to type {@code T[]}
408      * or throw an {@code IllegalArgumentException} if the cast fails. If the {@code extra} isn't
409      * present in the {@code intent}, return null.
410      */
411     @Nullable
getParcelableArrayExtraIfPresent( final Intent intent, String extra, @NonNull Class<T> clazz)412     private static <T extends Parcelable> T[] getParcelableArrayExtraIfPresent(
413             final Intent intent, String extra, @NonNull Class<T> clazz) throws
414                     IllegalArgumentException {
415         if (!intent.hasExtra(extra)) {
416             return null;
417         }
418 
419         T[] castResult = intent.getParcelableArrayExtra(extra, clazz);
420         if (castResult == null) {
421             Parcelable[] actualExtrasArray = intent.getParcelableArrayExtra(extra);
422             if (actualExtrasArray != null) {
423                 throw new IllegalArgumentException(
424                         String.format(
425                                 "%s is not of type %s[]: %s",
426                                 extra,
427                                 clazz.getSimpleName(),
428                                 Arrays.toString(actualExtrasArray)));
429             } else if (intent.getParcelableExtra(extra) != null) {
430                 throw new IllegalArgumentException(
431                         String.format(
432                                 "%s is not of type %s[] (or any array type): %s",
433                                 extra,
434                                 clazz.getSimpleName(),
435                                 intent.getParcelableExtra(extra)));
436             } else {
437                 throw new IllegalArgumentException(
438                         String.format(
439                                 "%s is not of type %s (or any Parcelable type): %s",
440                                 extra,
441                                 clazz.getSimpleName(),
442                                 intent.getExtras().get(extra)));
443             }
444         }
445 
446         return castResult;
447     }
448 
getTargetIntentFilter(final Intent intent)449     private static IntentFilter getTargetIntentFilter(final Intent intent) {
450         try {
451             String dataString = intent.getDataString();
452             if (intent.getType() == null) {
453                 if (!TextUtils.isEmpty(dataString)) {
454                     return new IntentFilter(intent.getAction(), dataString);
455                 }
456                 Log.e(TAG, "Failed to get target intent filter: intent data and type are null");
457                 return null;
458             }
459             IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType());
460             List<Uri> contentUris = new ArrayList<>();
461             if (Intent.ACTION_SEND.equals(intent.getAction())) {
462                 Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
463                 if (uri != null) {
464                     contentUris.add(uri);
465                 }
466             } else {
467                 List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
468                 if (uris != null) {
469                     contentUris.addAll(uris);
470                 }
471             }
472             for (Uri uri : contentUris) {
473                 intentFilter.addDataScheme(uri.getScheme());
474                 intentFilter.addDataAuthority(uri.getAuthority(), null);
475                 intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
476             }
477             return intentFilter;
478         } catch (Exception e) {
479             Log.e(TAG, "Failed to get target intent filter", e);
480             return null;
481         }
482     }
483 }
484