blob: 81481bf15b67e94317dd5ad382e9b0501ab6109a [file] [log] [blame]
/*
* 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;
}
}
}