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