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