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