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