| /* |
| * 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.annotation.NonNull; |
| import android.annotation.Nullable; |
| 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.ChooserTarget; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| 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 final Intent mTarget; |
| private final Pair<CharSequence, Integer> mTitleSpec; |
| private final Intent mReferrerFillInIntent; |
| private final ImmutableList<ComponentName> mFilteredComponentNames; |
| private final ImmutableList<ChooserTarget> mCallerChooserTargets; |
| 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, |
| final Uri referrer, |
| @Nullable final ComponentName nearbySharingComponent) { |
| final Intent requestedTarget = parseTargetIntentExtra( |
| clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); |
| mTarget = intentWithModifiedLaunchFlags(requestedTarget); |
| |
| 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); |
| |
| mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent); |
| |
| mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent); |
| |
| mRetainInOnStop = clientIntent.getBooleanExtra( |
| ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false); |
| |
| mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT); |
| |
| mTargetIntentFilter = getTargetIntentFilter(mTarget); |
| } |
| |
| 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(); |
| } |
| |
| @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; |
| } |
| |
| /** |
| * 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) ? com.android.internal.R.string.chooseActivity : 0; |
| |
| return Pair.create(requestedTitle, defaultTitleRes); |
| } |
| |
| private static ImmutableList<ComponentName> getFilteredComponentNames( |
| Intent clientIntent, @Nullable ComponentName nearbySharingComponent) { |
| Stream<ComponentName> filteredComponents = streamParcelableArrayExtra( |
| clientIntent, Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class, true, true); |
| |
| if (nearbySharingComponent != null) { |
| // Exclude Nearby from main list if chip is present, to avoid duplication. |
| // TODO: we don't have an explicit guarantee that the chip will be displayed just |
| // because we have a non-null component; that's ultimately determined by the preview |
| // layout. Maybe we can make that decision further upstream? |
| filteredComponents = Stream.concat( |
| filteredComponents, Stream.of(nearbySharingComponent)); |
| } |
| |
| return filteredComponents.collect(toImmutableList()); |
| } |
| |
| private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent( |
| Intent clientIntent) { |
| return |
| streamParcelableArrayExtra( |
| clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true) |
| .collect(toImmutableList()); |
| } |
| |
| 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 {@link warnOnTypeError} 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; |
| } |
| } |
| } |