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