|  | /* | 
|  | * Copyright (C) 2008 The Android Open Source Project | 
|  | * | 
|  | * Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | * you may not use this file except in compliance with the License. | 
|  | * You may obtain a copy of the License at | 
|  | * | 
|  | *      http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, software | 
|  | * distributed under the License is distributed on an "AS IS" BASIS, | 
|  | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | * See the License for the specific language governing permissions and | 
|  | * limitations under the License. | 
|  | */ | 
|  |  | 
|  | package com.android.intentresolver; | 
|  |  | 
|  | import android.content.ComponentName; | 
|  | import android.content.Intent; | 
|  | import android.content.IntentFilter; | 
|  | import android.content.IntentSender; | 
|  | import android.net.Uri; | 
|  | import android.os.Bundle; | 
|  | import android.os.Parcelable; | 
|  | import android.os.PatternMatcher; | 
|  | import android.service.chooser.ChooserAction; | 
|  | import android.service.chooser.ChooserTarget; | 
|  | import android.text.TextUtils; | 
|  | import android.util.Log; | 
|  | import android.util.Pair; | 
|  |  | 
|  | import androidx.annotation.NonNull; | 
|  | import androidx.annotation.Nullable; | 
|  |  | 
|  | import com.android.intentresolver.util.UriFilters; | 
|  |  | 
|  | import com.google.common.collect.ImmutableList; | 
|  |  | 
|  | import java.net.URISyntaxException; | 
|  | import java.util.ArrayList; | 
|  | import java.util.Arrays; | 
|  | import java.util.List; | 
|  | import java.util.stream.Collector; | 
|  | import java.util.stream.Collectors; | 
|  | import java.util.stream.Stream; | 
|  |  | 
|  | /** | 
|  | * Utility to parse and validate parameters from the client-supplied {@link Intent} that launched | 
|  | * the Sharesheet {@link ChooserActivity}. The validated parameters are stored as immutable ivars. | 
|  | * | 
|  | * TODO: field nullability in this class reflects legacy use, and typically would indicate that the | 
|  | * client's intent didn't provide the respective data. In some cases we may be able to provide | 
|  | * defaults instead of nulls -- especially for methods that return nullable lists or arrays, if the | 
|  | * client code could instead handle empty collections equally well. | 
|  | * | 
|  | * TODO: some of these fields (especially getTargetIntent() and any other getters that delegate to | 
|  | * it internally) differ from the legacy model because they're computed directly from the initial | 
|  | * Chooser intent, where in the past they've been relayed up to ResolverActivity and then retrieved | 
|  | * through methods on the base class. The base always seems to return them exactly as they were | 
|  | * provided, so this should be safe -- and clients can reasonably switch to retrieving through these | 
|  | * parameters instead. For now, the other convention is still used in some places. Ideally we'd like | 
|  | * to normalize on a single source of truth, but we'll have to clean up the delegation up to the | 
|  | * resolver (or perhaps this needs to be a subclass of some `ResolverRequestParameters` class?). | 
|  | */ | 
|  | public class ChooserRequestParameters { | 
|  | private static final String TAG = "ChooserActivity"; | 
|  |  | 
|  | private static final int LAUNCH_FLAGS_FOR_SEND_ACTION = | 
|  | Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; | 
|  | private static final int MAX_CHOOSER_ACTIONS = 5; | 
|  |  | 
|  | private final Intent mTarget; | 
|  | private final String mReferrerPackageName; | 
|  | private final Pair<CharSequence, Integer> mTitleSpec; | 
|  | private final Intent mReferrerFillInIntent; | 
|  | private final ImmutableList<ComponentName> mFilteredComponentNames; | 
|  | private final ImmutableList<ChooserTarget> mCallerChooserTargets; | 
|  | private final @NonNull ImmutableList<ChooserAction> mChooserActions; | 
|  | private final ChooserAction mModifyShareAction; | 
|  | private final boolean mRetainInOnStop; | 
|  |  | 
|  | @Nullable | 
|  | private final ImmutableList<Intent> mAdditionalTargets; | 
|  |  | 
|  | @Nullable | 
|  | private final Bundle mReplacementExtras; | 
|  |  | 
|  | @Nullable | 
|  | private final ImmutableList<Intent> mInitialIntents; | 
|  |  | 
|  | @Nullable | 
|  | private final IntentSender mChosenComponentSender; | 
|  |  | 
|  | @Nullable | 
|  | private final IntentSender mRefinementIntentSender; | 
|  |  | 
|  | @Nullable | 
|  | private final String mSharedText; | 
|  |  | 
|  | @Nullable | 
|  | private final IntentFilter mTargetIntentFilter; | 
|  |  | 
|  | public ChooserRequestParameters( | 
|  | final Intent clientIntent, | 
|  | String referrerPackageName, | 
|  | final Uri referrer) { | 
|  | final Intent requestedTarget = parseTargetIntentExtra( | 
|  | clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); | 
|  | mTarget = intentWithModifiedLaunchFlags(requestedTarget); | 
|  |  | 
|  | mReferrerPackageName = referrerPackageName; | 
|  |  | 
|  | mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent( | 
|  | clientIntent, Intent.EXTRA_ALTERNATE_INTENTS); | 
|  |  | 
|  | mReplacementExtras = clientIntent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); | 
|  |  | 
|  | mTitleSpec = makeTitleSpec( | 
|  | clientIntent.getCharSequenceExtra(Intent.EXTRA_TITLE), | 
|  | isSendAction(mTarget.getAction())); | 
|  |  | 
|  | mInitialIntents = intentsWithModifiedLaunchFlagsFromExtraIfPresent( | 
|  | clientIntent, Intent.EXTRA_INITIAL_INTENTS); | 
|  |  | 
|  | mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer); | 
|  |  | 
|  | mChosenComponentSender = clientIntent.getParcelableExtra( | 
|  | Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); | 
|  | mRefinementIntentSender = clientIntent.getParcelableExtra( | 
|  | Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); | 
|  |  | 
|  | ComponentName[] filteredComponents = clientIntent.getParcelableArrayExtra( | 
|  | Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class); | 
|  | mFilteredComponentNames = filteredComponents != null | 
|  | ? ImmutableList.copyOf(filteredComponents) | 
|  | : ImmutableList.of(); | 
|  |  | 
|  | mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent); | 
|  |  | 
|  | mRetainInOnStop = clientIntent.getBooleanExtra( | 
|  | ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false); | 
|  |  | 
|  | mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT); | 
|  |  | 
|  | mTargetIntentFilter = getTargetIntentFilter(mTarget); | 
|  |  | 
|  | mChooserActions = getChooserActions(clientIntent); | 
|  | mModifyShareAction = getModifyShareAction(clientIntent); | 
|  | } | 
|  |  | 
|  | public Intent getTargetIntent() { | 
|  | return mTarget; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public String getTargetAction() { | 
|  | return getTargetIntent().getAction(); | 
|  | } | 
|  |  | 
|  | public boolean isSendActionTarget() { | 
|  | return isSendAction(getTargetAction()); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public String getTargetType() { | 
|  | return getTargetIntent().getType(); | 
|  | } | 
|  |  | 
|  | public String getReferrerPackageName() { | 
|  | return mReferrerPackageName; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public CharSequence getTitle() { | 
|  | return mTitleSpec.first; | 
|  | } | 
|  |  | 
|  | public int getDefaultTitleResource() { | 
|  | return mTitleSpec.second; | 
|  | } | 
|  |  | 
|  | public Intent getReferrerFillInIntent() { | 
|  | return mReferrerFillInIntent; | 
|  | } | 
|  |  | 
|  | public ImmutableList<ComponentName> getFilteredComponentNames() { | 
|  | return mFilteredComponentNames; | 
|  | } | 
|  |  | 
|  | public ImmutableList<ChooserTarget> getCallerChooserTargets() { | 
|  | return mCallerChooserTargets; | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | public ImmutableList<ChooserAction> getChooserActions() { | 
|  | return mChooserActions; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public ChooserAction getModifyShareAction() { | 
|  | return mModifyShareAction; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Whether the {@link ChooserActivity#EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested. | 
|  | */ | 
|  | public boolean shouldRetainInOnStop() { | 
|  | return mRetainInOnStop; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * TODO: this returns a nullable array for convenience, but if the legacy APIs can be | 
|  | * refactored, returning {@link #mAdditionalTargets} directly is simpler and safer. | 
|  | */ | 
|  | @Nullable | 
|  | public Intent[] getAdditionalTargets() { | 
|  | return (mAdditionalTargets == null) ? null : mAdditionalTargets.toArray(new Intent[0]); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public Bundle getReplacementExtras() { | 
|  | return mReplacementExtras; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * TODO: this returns a nullable array for convenience, but if the legacy APIs can be | 
|  | * refactored, returning {@link #mInitialIntents} directly is simpler and safer. | 
|  | */ | 
|  | @Nullable | 
|  | public Intent[] getInitialIntents() { | 
|  | return (mInitialIntents == null) ? null : mInitialIntents.toArray(new Intent[0]); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public IntentSender getChosenComponentSender() { | 
|  | return mChosenComponentSender; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public IntentSender getRefinementIntentSender() { | 
|  | return mRefinementIntentSender; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public String getSharedText() { | 
|  | return mSharedText; | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | public IntentFilter getTargetIntentFilter() { | 
|  | return mTargetIntentFilter; | 
|  | } | 
|  |  | 
|  | private static boolean isSendAction(@Nullable String action) { | 
|  | return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)); | 
|  | } | 
|  |  | 
|  | private static Intent parseTargetIntentExtra(@Nullable Parcelable targetParcelable) { | 
|  | if (targetParcelable instanceof Uri) { | 
|  | try { | 
|  | targetParcelable = Intent.parseUri(targetParcelable.toString(), | 
|  | Intent.URI_INTENT_SCHEME); | 
|  | } catch (URISyntaxException ex) { | 
|  | throw new IllegalArgumentException("Failed to parse EXTRA_INTENT from URI", ex); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (!(targetParcelable instanceof Intent)) { | 
|  | throw new IllegalArgumentException( | 
|  | "EXTRA_INTENT is neither an Intent nor a Uri: " + targetParcelable); | 
|  | } | 
|  |  | 
|  | return ((Intent) targetParcelable); | 
|  | } | 
|  |  | 
|  | private static Intent intentWithModifiedLaunchFlags(Intent intent) { | 
|  | if (isSendAction(intent.getAction())) { | 
|  | intent.addFlags(LAUNCH_FLAGS_FOR_SEND_ACTION); | 
|  | } | 
|  | return intent; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Build a pair of values specifying the title to use from the client request. The first | 
|  | * ({@link CharSequence}) value is the client-specified title, if there was one and their | 
|  | * requested target <em>wasn't</em> a send action; otherwise it is null. The second value is | 
|  | * the resource ID of a default title string; this is nonzero only if the first value is null. | 
|  | * | 
|  | * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate}, or | 
|  | * create a real type (not {@link Pair}) to express the semantics described in this comment. | 
|  | */ | 
|  | private static Pair<CharSequence, Integer> makeTitleSpec( | 
|  | @Nullable CharSequence requestedTitle, boolean hasSendActionTarget) { | 
|  | if (hasSendActionTarget && (requestedTitle != null)) { | 
|  | // Do not allow the title to be changed when sharing content | 
|  | Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a" | 
|  | + " preview title by using EXTRA_TITLE property of the wrapped" | 
|  | + " EXTRA_INTENT."); | 
|  | requestedTitle = null; | 
|  | } | 
|  |  | 
|  | int defaultTitleRes = (requestedTitle == null) ? R.string.chooseActivity : 0; | 
|  |  | 
|  | return Pair.create(requestedTitle, defaultTitleRes); | 
|  | } | 
|  |  | 
|  | private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent( | 
|  | Intent clientIntent) { | 
|  | return | 
|  | streamParcelableArrayExtra( | 
|  | clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true) | 
|  | .collect(toImmutableList()); | 
|  | } | 
|  |  | 
|  | @NonNull | 
|  | private static ImmutableList<ChooserAction> getChooserActions(Intent intent) { | 
|  | return streamParcelableArrayExtra( | 
|  | intent, | 
|  | Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, | 
|  | ChooserAction.class, | 
|  | true, | 
|  | true) | 
|  | .filter(UriFilters::hasValidIcon) | 
|  | .limit(MAX_CHOOSER_ACTIONS) | 
|  | .collect(toImmutableList()); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | private static ChooserAction getModifyShareAction(Intent intent) { | 
|  | try { | 
|  | return intent.getParcelableExtra( | 
|  | Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, | 
|  | ChooserAction.class); | 
|  | } catch (Throwable t) { | 
|  | Log.w( | 
|  | TAG, | 
|  | "Unable to retrieve Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION argument", | 
|  | t); | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() { | 
|  | return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf); | 
|  | } | 
|  |  | 
|  | @Nullable | 
|  | private static ImmutableList<Intent> intentsWithModifiedLaunchFlagsFromExtraIfPresent( | 
|  | Intent clientIntent, String extra) { | 
|  | Stream<Intent> intents = | 
|  | streamParcelableArrayExtra(clientIntent, extra, Intent.class, true, false); | 
|  | if (intents == null) { | 
|  | return null; | 
|  | } | 
|  | return intents | 
|  | .map(ChooserRequestParameters::intentWithModifiedLaunchFlags) | 
|  | .collect(toImmutableList()); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Make a {@link Stream} of the {@link Parcelable} objects given in the provided {@link Intent} | 
|  | * as the optional parcelable array extra with key {@code extra}. The stream elements, if any, | 
|  | * are all of the type specified by {@code clazz}. | 
|  | * | 
|  | * @param intent The intent that may contain the optional extras. | 
|  | * @param extra The extras key to identify the parcelable array. | 
|  | * @param clazz A class that is assignable from any elements in the result stream. | 
|  | * @param warnOnTypeError Whether to log a warning (and ignore) if the client extra doesn't have | 
|  | * the required type. If false, throw an {@link IllegalArgumentException} if the extra is | 
|  | * non-null but can't be assigned to variables of type {@code T}. | 
|  | * @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't | 
|  | * present in the intent (or if it had the wrong type, but <em>warnOnTypeError</em> is true). | 
|  | * If false, return null in these cases, and only return an empty stream if the intent | 
|  | * explicitly provided an empty array for the specified extra. | 
|  | */ | 
|  | @Nullable | 
|  | private static <T extends Parcelable> Stream<T> streamParcelableArrayExtra( | 
|  | final Intent intent, | 
|  | String extra, | 
|  | @NonNull Class<T> clazz, | 
|  | boolean warnOnTypeError, | 
|  | boolean streamEmptyIfNull) { | 
|  | T[] result = null; | 
|  |  | 
|  | try { | 
|  | result = getParcelableArrayExtraIfPresent(intent, extra, clazz); | 
|  | } catch (IllegalArgumentException e) { | 
|  | if (warnOnTypeError) { | 
|  | Log.w(TAG, "Ignoring client-requested " + extra, e); | 
|  | } else { | 
|  | throw e; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (result != null) { | 
|  | return Arrays.stream(result); | 
|  | } else if (streamEmptyIfNull) { | 
|  | return Stream.empty(); | 
|  | } else { | 
|  | return null; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * If the specified {@code extra} is provided in the {@code intent}, cast it to type {@code T[]} | 
|  | * or throw an {@code IllegalArgumentException} if the cast fails. If the {@code extra} isn't | 
|  | * present in the {@code intent}, return null. | 
|  | */ | 
|  | @Nullable | 
|  | private static <T extends Parcelable> T[] getParcelableArrayExtraIfPresent( | 
|  | final Intent intent, String extra, @NonNull Class<T> clazz) throws | 
|  | IllegalArgumentException { | 
|  | if (!intent.hasExtra(extra)) { | 
|  | return null; | 
|  | } | 
|  |  | 
|  | T[] castResult = intent.getParcelableArrayExtra(extra, clazz); | 
|  | if (castResult == null) { | 
|  | Parcelable[] actualExtrasArray = intent.getParcelableArrayExtra(extra); | 
|  | if (actualExtrasArray != null) { | 
|  | throw new IllegalArgumentException( | 
|  | String.format( | 
|  | "%s is not of type %s[]: %s", | 
|  | extra, | 
|  | clazz.getSimpleName(), | 
|  | Arrays.toString(actualExtrasArray))); | 
|  | } else if (intent.getParcelableExtra(extra) != null) { | 
|  | throw new IllegalArgumentException( | 
|  | String.format( | 
|  | "%s is not of type %s[] (or any array type): %s", | 
|  | extra, | 
|  | clazz.getSimpleName(), | 
|  | intent.getParcelableExtra(extra))); | 
|  | } else { | 
|  | throw new IllegalArgumentException( | 
|  | String.format( | 
|  | "%s is not of type %s (or any Parcelable type): %s", | 
|  | extra, | 
|  | clazz.getSimpleName(), | 
|  | intent.getExtras().get(extra))); | 
|  | } | 
|  | } | 
|  |  | 
|  | return castResult; | 
|  | } | 
|  |  | 
|  | private static IntentFilter getTargetIntentFilter(final Intent intent) { | 
|  | try { | 
|  | String dataString = intent.getDataString(); | 
|  | if (intent.getType() == null) { | 
|  | if (!TextUtils.isEmpty(dataString)) { | 
|  | return new IntentFilter(intent.getAction(), dataString); | 
|  | } | 
|  | Log.e(TAG, "Failed to get target intent filter: intent data and type are null"); | 
|  | return null; | 
|  | } | 
|  | IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType()); | 
|  | List<Uri> contentUris = new ArrayList<>(); | 
|  | if (Intent.ACTION_SEND.equals(intent.getAction())) { | 
|  | Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); | 
|  | if (uri != null) { | 
|  | contentUris.add(uri); | 
|  | } | 
|  | } else { | 
|  | List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); | 
|  | if (uris != null) { | 
|  | contentUris.addAll(uris); | 
|  | } | 
|  | } | 
|  | for (Uri uri : contentUris) { | 
|  | intentFilter.addDataScheme(uri.getScheme()); | 
|  | intentFilter.addDataAuthority(uri.getAuthority(), null); | 
|  | intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); | 
|  | } | 
|  | return intentFilter; | 
|  | } catch (Exception e) { | 
|  | Log.e(TAG, "Failed to get target intent filter", e); | 
|  | return null; | 
|  | } | 
|  | } | 
|  | } |