1 /* 2 * Copyright (C) 2019 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.chooser; 18 19 20 import static android.security.Flags.preventIntentRedirect; 21 22 import android.app.Activity; 23 import android.app.ActivityManager; 24 import android.app.prediction.AppTarget; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.SharedPreferences; 29 import android.content.pm.ResolveInfo; 30 import android.content.pm.ShortcutInfo; 31 import android.content.pm.ShortcutManager; 32 import android.graphics.drawable.Drawable; 33 import android.os.Bundle; 34 import android.os.RemoteException; 35 import android.os.UserHandle; 36 import android.service.chooser.ChooserTarget; 37 import android.text.TextUtils; 38 import android.util.HashedStringCache; 39 40 import androidx.annotation.Nullable; 41 42 import com.android.intentresolver.ChooserListAdapter; 43 import com.android.intentresolver.ChooserRefinementManager; 44 import com.android.intentresolver.ResolverActivity; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Objects; 49 50 /** 51 * A single target as represented in the chooser. 52 */ 53 public interface TargetInfo { 54 55 /** 56 * Container for a {@link TargetInfo}'s (potentially) mutable icon state. This is provided to 57 * encapsulate the state so that the {@link TargetInfo} itself can be "immutable" (in some 58 * sense) as long as it always returns the same {@link IconHolder} instance. 59 * 60 * TODO: move "stateful" responsibilities out to clients; for more info see the Javadoc comment 61 * on {@link #getDisplayIconHolder()}. 62 */ 63 interface IconHolder { 64 /** @return the icon (if it's already loaded, or statically available), or null. */ 65 @Nullable getDisplayIcon()66 Drawable getDisplayIcon(); 67 68 /** 69 * @param icon the icon to return on subsequent calls to {@link #getDisplayIcon()}. 70 * Implementations may discard this request as a no-op if they don't support setting. 71 */ setDisplayIcon(@ullable Drawable icon)72 void setDisplayIcon(@Nullable Drawable icon); 73 } 74 75 /** A simple mutable-container implementation of {@link IconHolder}. */ 76 final class SettableIconHolder implements IconHolder { 77 @Nullable 78 private Drawable mDisplayIcon; 79 80 @Nullable getDisplayIcon()81 public Drawable getDisplayIcon() { 82 return mDisplayIcon; 83 } 84 setDisplayIcon(@ullable Drawable icon)85 public void setDisplayIcon(@Nullable Drawable icon) { 86 mDisplayIcon = icon; 87 } 88 } 89 90 /** 91 * Get the resolved intent that represents this target. Note that this may not be the 92 * intent that will be launched by calling one of the <code>start</code> methods provided; 93 * this is the intent that will be credited with the launch. 94 * 95 * @return the resolved intent for this target 96 */ getResolvedIntent()97 Intent getResolvedIntent(); 98 99 /** 100 * Get the target intent, the one that will be used with one of the <code>start</code> methods. 101 * @return the intent with target will be launced with. 102 */ getTargetIntent()103 @Nullable Intent getTargetIntent(); 104 105 /** 106 * Get the resolved component name that represents this target. Note that this may not 107 * be the component that will be directly launched by calling one of the <code>start</code> 108 * methods provided; this is the component that will be credited with the launch. This may be 109 * null if the target was specified by a caller-provided {@link ChooserTarget} that we failed to 110 * resolve to a component on the system. 111 * 112 * @return the resolved ComponentName for this target 113 */ 114 @Nullable getResolvedComponentName()115 ComponentName getResolvedComponentName(); 116 117 /** 118 * If this target was historically built from a (now-deprecated) {@link ChooserTarget} record, 119 * get the {@link ComponentName} that would've been provided by that record. 120 * 121 * TODO: for (historical) {@link ChooserTargetInfo} targets, this differs from the result of 122 * {@link #getResolvedComponentName()} only for caller-provided targets that we fail to resolve; 123 * then this returns the name of the component that was requested, and the other returns null. 124 * At the time of writing, this method is only called in contexts where the client knows that 125 * the target was a historical {@link ChooserTargetInfo}. Thus this method could be removed and 126 * all clients consolidated on the other, if we have some alternate mechanism of tracking this 127 * discrepancy; or if we know that the distinction won't apply in the conditions when we call 128 * this method; or if we determine that tracking the distinction isn't a requirement for us. 129 */ 130 @Nullable getChooserTargetComponentName()131 default ComponentName getChooserTargetComponentName() { 132 return null; 133 } 134 135 /** 136 * Start the activity referenced by this target as if the Activity's caller was performing the 137 * start operation. 138 * 139 * @param activity calling Activity (actually) performing the launch 140 * @param options ActivityOptions bundle 141 * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller 142 * @return true if the start completed successfully 143 */ startAsCaller(Activity activity, Bundle options, int userId)144 boolean startAsCaller(Activity activity, Bundle options, int userId); 145 146 /** 147 * Start the activity referenced by this target as a given user. 148 * 149 * @param activity calling activity performing the launch 150 * @param options ActivityOptions bundle 151 * @param user handle for the user to start the activity as 152 * @return true if the start completed successfully 153 */ startAsUser(Activity activity, Bundle options, UserHandle user)154 boolean startAsUser(Activity activity, Bundle options, UserHandle user); 155 156 /** 157 * Return the ResolveInfo about how and why this target matched the original query 158 * for available targets. 159 * 160 * @return ResolveInfo representing this target's match 161 */ getResolveInfo()162 ResolveInfo getResolveInfo(); 163 164 /** 165 * Return the human-readable text label for this target. 166 * 167 * @return user-visible target label 168 */ getDisplayLabel()169 CharSequence getDisplayLabel(); 170 171 /** 172 * Return any extended info for this target. This may be used to disambiguate 173 * otherwise identical targets. 174 * 175 * @return human-readable disambig string or null if none present 176 */ getExtendedInfo()177 CharSequence getExtendedInfo(); 178 179 /** 180 * @return the {@link IconHolder} for the icon used to represent this target, including badge. 181 * 182 * TODO: while the {@link TargetInfo} may be immutable in always returning the same instance of 183 * {@link IconHolder} here, the holder itself is mutable state, and could become a problem if we 184 * ever rely on {@link TargetInfo} immutability elsewhere. Ideally, the {@link TargetInfo} 185 * should provide an immutable "spec" that tells clients <em>how</em> to load the appropriate 186 * icon, while leaving the load itself to some external component. 187 */ getDisplayIconHolder()188 IconHolder getDisplayIconHolder(); 189 190 /** 191 * @return true if display icon is available. 192 */ hasDisplayIcon()193 default boolean hasDisplayIcon() { 194 return getDisplayIconHolder().getDisplayIcon() != null; 195 } 196 197 /** 198 * Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager} 199 * received from the caller's refinement flow. This may succeed only if the target has a source 200 * intent that matches the filtering parameters of the proposed refinement (according to 201 * {@link Intent#filterEquals}). Then the first such match is the "base intent," and the 202 * proposed refinement is merged into that base (via {@link Intent#fillIn}; this can never 203 * result in a change to the {@link Intent#filterEquals} status of the base, but may e.g. add 204 * new "extras" that weren't previously given in the base intent). 205 * 206 * @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of 207 * merging the refinement into the best-matching source intent, if possible. If there is no 208 * suitable match for the proposed refinement, or if merging fails for any other reason, this 209 * returns null. 210 * 211 * @see android.content.Intent#fillIn(Intent, int) 212 */ 213 @Nullable tryToCloneWithAppliedRefinement(Intent proposedRefinement)214 TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement); 215 216 /** 217 * @return the list of supported source intents deduped against this single target 218 */ getAllSourceIntents()219 List<Intent> getAllSourceIntents(); 220 221 /** 222 * @return the one or more {@link DisplayResolveInfo}s that this target represents in the UI. 223 * 224 * TODO: clarify the semantics of the {@link DisplayResolveInfo} branch of {@link TargetInfo}'s 225 * class hierarchy. Why is it that {@link MultiDisplayResolveInfo} can stand in for some 226 * "virtual" {@link DisplayResolveInfo} targets that aren't individually represented in the UI, 227 * but OTOH a {@link ChooserTargetInfo} (which doesn't inherit from {@link DisplayResolveInfo}) 228 * can't provide its own UI treatment, and instead needs us to reach into its composed-in 229 * info via {@link #getDisplayResolveInfo()}? It seems like {@link DisplayResolveInfo} may be 230 * required to populate views in our UI, while {@link ChooserTargetInfo} may carry some other 231 * metadata. For non-{@link ChooserTargetInfo} targets (e.g. in {@link ResolverActivity}) the 232 * "naked" {@link DisplayResolveInfo} might also be taken to provide some of this metadata, but 233 * this presents a denormalization hazard since the "UI info" ({@link DisplayResolveInfo}) that 234 * represents a {@link ChooserTargetInfo} might provide different values than its enclosing 235 * {@link ChooserTargetInfo} (as they both implement {@link TargetInfo}). We could try to 236 * address this by splitting {@link DisplayResolveInfo} into two types; one (which implements 237 * the same {@link TargetInfo} interface as {@link ChooserTargetInfo}) provides the previously- 238 * implicit "metadata", and the other provides only the UI treatment for a target of any type 239 * (taking over the respective methods that previously belonged to {@link TargetInfo}). 240 */ getAllDisplayTargets()241 ArrayList<DisplayResolveInfo> getAllDisplayTargets(); 242 243 /** 244 * @return true if this target cannot be selected by the user 245 */ isSuspended()246 boolean isSuspended(); 247 248 /** 249 * @return true if this target should be pinned to the front by the request of the user 250 */ isPinned()251 boolean isPinned(); 252 253 /** 254 * Determine whether two targets represent "similar" content that could be de-duped. 255 * Note an earlier version of this code cautioned maintainers, 256 * "do not label as 'equals', since this doesn't quite work as intended with java 8." 257 * This seems to refer to the rule that interfaces can't provide defaults that conflict with the 258 * definitions of "real" methods in {@code java.lang.Object}, and (if desired) it could be 259 * presumably resolved by converting {@code TargetInfo} from an interface to an abstract class. 260 */ isSimilar(TargetInfo other)261 default boolean isSimilar(TargetInfo other) { 262 if (other == null) { 263 return false; 264 } 265 266 // TODO: audit usage and try to reconcile a behavior that doesn't depend on the legacy 267 // subclass type. Note that the `isSimilar()` method was pulled up from the legacy 268 // `ChooserTargetInfo`, so no legacy behavior currently depends on calling `isSimilar()` on 269 // an instance where `isChooserTargetInfo()` would return false (although technically it may 270 // have been possible for the `other` target to be of a different type). Thus we have 271 // flexibility in defining the similarity conditions between pairs of non "chooser" targets. 272 if (isChooserTargetInfo()) { 273 return other.isChooserTargetInfo() 274 && Objects.equals( 275 getChooserTargetComponentName(), other.getChooserTargetComponentName()) 276 && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel()) 277 && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo()); 278 } else { 279 return !other.isChooserTargetInfo() && Objects.equals(this, other); 280 } 281 } 282 283 /** 284 * @return the target score, including any Chooser-specific modifications that may have been 285 * applied (either overriding by special-case for "non-selectable" targets, or by twiddling the 286 * scores of "selectable" targets in {@link ChooserListAdapter}). Higher scores are "better." 287 * Targets that aren't intended for ranking/scoring should return a negative value. 288 */ getModifiedScore()289 default float getModifiedScore() { 290 return -0.1f; 291 } 292 293 /** 294 * @return the {@link ShortcutInfo} for any shortcut associated with this target. 295 */ 296 @Nullable getDirectShareShortcutInfo()297 default ShortcutInfo getDirectShareShortcutInfo() { 298 return null; 299 } 300 301 /** 302 * @return the ID of the shortcut represented by this target, or null if the target didn't come 303 * from a {@link ShortcutManager} shortcut. 304 */ 305 @Nullable getDirectShareShortcutId()306 default String getDirectShareShortcutId() { 307 ShortcutInfo shortcut = getDirectShareShortcutInfo(); 308 if (shortcut == null) { 309 return null; 310 } 311 return shortcut.getId(); 312 } 313 314 /** 315 * @return the {@link AppTarget} metadata if this target was sourced from App Prediction 316 * service, or null otherwise. 317 */ 318 @Nullable getDirectShareAppTarget()319 default AppTarget getDirectShareAppTarget() { 320 return null; 321 } 322 323 /** 324 * Get more info about this target in the form of a {@link DisplayResolveInfo}, if available. 325 * TODO: this seems to return non-null only for ChooserTargetInfo subclasses. Determine the 326 * meaning of a TargetInfo (ChooserTargetInfo) embedding another kind of TargetInfo 327 * (DisplayResolveInfo) in this way, and - at least - improve this documentation; OTOH this 328 * probably indicates an opportunity to simplify or better separate these APIs. (For example, 329 * targets that <em>don't</em> descend from ChooserTargetInfo instead descend directly from 330 * DisplayResolveInfo; should they return `this`? Do we always use DisplayResolveInfo to 331 * represent visual properties, and then either assume some implicit metadata properties *or* 332 * embed that visual representation within a ChooserTargetInfo to carry additional metadata? If 333 * that's the case, maybe we could decouple by saying that all TargetInfos compose-in their 334 * visual representation [as a DisplayResolveInfo, now the root of its own class hierarchy] and 335 * then add a new TargetInfo type that explicitly represents the "implicit metadata" that we 336 * previously assumed for "naked DisplayResolveInfo targets" that weren't wrapped as 337 * ChooserTargetInfos. Or does all this complexity disappear once we stop relying on the 338 * deprecated ChooserTarget type?) 339 */ 340 @Nullable getDisplayResolveInfo()341 default DisplayResolveInfo getDisplayResolveInfo() { 342 return null; 343 } 344 345 /** 346 * @return true if this target represents a legacy {@code ChooserTargetInfo}. These objects were 347 * historically documented as representing "[a] TargetInfo for Direct Share." However, not all 348 * of these targets are actually *valid* for direct share; e.g. some represent "empty" items 349 * (although perhaps only for display in the Direct Share UI?). In even earlier versions, these 350 * targets may also have been results from peers in the (now-deprecated/unsupported) 351 * {@code ChooserTargetService} ecosystem; even though we no longer use these services, we're 352 * still shoehorning other target data into the deprecated {@link ChooserTarget} structure for 353 * compatibility with some internal APIs. 354 * TODO: refactor to clarify the semantics of any target for which this method returns true 355 * (e.g., are they characterized by their application in the Direct Share UI?), and to remove 356 * the scaffolding that adapts to and from the {@link ChooserTarget} structure. Eventually, we 357 * expect to remove this method (and others that strictly indicate legacy subclass roles) in 358 * favor of a more semantic design that expresses the purpose and distinctions in those roles. 359 */ isChooserTargetInfo()360 default boolean isChooserTargetInfo() { 361 return false; 362 } 363 364 /** 365 * @return true if this target represents a legacy {@code DisplayResolveInfo}. These objects 366 * were historically documented as an augmented "TargetInfo plus additional information needed 367 * to render it (such as icon and label) and resolve it to an activity." That description in no 368 * way distinguishes from the base {@code TargetInfo} API. At the time of writing, these objects 369 * are most-clearly defined by their opposite; this returns true for exactly those instances of 370 * {@code TargetInfo} where {@link #isChooserTargetInfo()} returns false (these conditions are 371 * complementary because they correspond to the immediate {@code TargetInfo} child types that 372 * historically partitioned all concrete {@code TargetInfo} implementations). These may(?) 373 * represent any target displayed somewhere other than the Direct Share UI. 374 */ isDisplayResolveInfo()375 default boolean isDisplayResolveInfo() { 376 return false; 377 } 378 379 /** 380 * @return true if this target represents a legacy {@code MultiDisplayResolveInfo}. These 381 * objects were historically documented as representing "a 'stack' of chooser targets for 382 * various activities within the same component." For historical reasons this currently can 383 * return true only if {@link #isDisplayResolveInfo()} returns true (because the legacy classes 384 * shared an inheritance relationship), but new code should avoid relying on that relationship 385 * since these APIs are "in transition." 386 */ isMultiDisplayResolveInfo()387 default boolean isMultiDisplayResolveInfo() { 388 return false; 389 } 390 391 /** 392 * @return true if this target represents a legacy {@code SelectableTargetInfo}. Note that this 393 * is defined for legacy compatibility and may not conform to other notions of a "selectable" 394 * target. For historical reasons, this method and {@link #isNotSelectableTargetInfo()} only 395 * partition the {@code TargetInfo} instances for which {@link #isChooserTargetInfo()} returns 396 * true; otherwise <em>both</em> methods return false. 397 * TODO: define selectability for targets not historically from {@code ChooserTargetInfo}, 398 * then attempt to replace this with a new method like {@code TargetInfo#isSelectable()} that 399 * actually partitions <em>all</em> target types (after updating client usage as needed). 400 */ isSelectableTargetInfo()401 default boolean isSelectableTargetInfo() { 402 return false; 403 } 404 405 /** 406 * @return true if this target represents a legacy {@code NotSelectableTargetInfo} (i.e., a 407 * target where {@link #isChooserTargetInfo()} is true but {@link #isSelectableTargetInfo()} is 408 * false). For more information on how this divides the space of targets, see the Javadoc for 409 * {@link #isSelectableTargetInfo()}. 410 */ isNotSelectableTargetInfo()411 default boolean isNotSelectableTargetInfo() { 412 return false; 413 } 414 415 /** 416 * @return true if this target represents a legacy {@code ChooserActivity#EmptyTargetInfo}. Note 417 * that this is defined for legacy compatibility and may not conform to other notions of an 418 * "empty" target. 419 */ isEmptyTargetInfo()420 default boolean isEmptyTargetInfo() { 421 return false; 422 } 423 424 /** 425 * @return true if this target represents a legacy {@code ChooserActivity#PlaceHolderTargetInfo} 426 * (defined only for compatibility with historic use in {@link ChooserListAdapter}). For 427 * historic reasons (owing to a legacy subclass relationship) this can return true only if 428 * {@link #isNotSelectableTargetInfo()} also returns true. 429 */ isPlaceHolderTargetInfo()430 default boolean isPlaceHolderTargetInfo() { 431 return false; 432 } 433 434 /** 435 * @return true if this target should be logged with the "direct_share" metrics category in 436 * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch}. This is defined for legacy 437 * compatibility and is <em>not</em> likely to be a good indicator of whether this is actually a 438 * "direct share" target (e.g. because it historically also applies to "empty" and "placeholder" 439 * targets). 440 */ isInDirectShareMetricsCategory()441 default boolean isInDirectShareMetricsCategory() { 442 return isChooserTargetInfo(); 443 } 444 445 /** 446 * @param context caller's context, to provide the {@link SharedPreferences} for use by the 447 * {@link HashedStringCache}. 448 * @return a hashed ID that should be logged along with our target-selection metrics, or null. 449 * The contents of the plaintext are defined for historical reasons, "the package name + target 450 * name to answer the question if most users share to mostly the same person 451 * or to a bunch of different people." Clients should consider this as opaque data for logging 452 * only; they should not rely on any particular semantics about the value. 453 */ getHashedTargetIdForMetrics(Context context)454 default HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { 455 return null; 456 } 457 458 /** 459 * Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called 460 * before launching the intent as another user. 461 */ prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId)462 static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) { 463 final int currentUserId = UserHandle.myUserId(); 464 if (targetUserId != currentUserId) { 465 intent.fixUris(currentUserId); 466 } 467 } 468 469 /** 470 * refreshes intent's creatorToken with its current intent key fields. This allows 471 * ChooserActivity to still keep original creatorToken's creator uid after making changes to 472 * the intent and still keep it valid. 473 * @param intent the intent's creatorToken needs to up refreshed. 474 */ refreshIntentCreatorToken(Intent intent)475 static void refreshIntentCreatorToken(Intent intent) { 476 if (!preventIntentRedirect()) return; 477 try { 478 intent.setCreatorToken(ActivityManager.getService().refreshIntentCreatorToken( 479 intent.cloneForCreatorToken())); 480 } catch (RemoteException e) { 481 throw new RuntimeException("Failure from system", e); 482 } 483 } 484 485 /** 486 * Derive a "complete" intent from a proposed `refinement` intent by merging it into a matching 487 * `base` intent, without modifying the filter-equality properties of the `base` intent, while 488 * still allowing the `refinement` to replace Share "payload" fields. 489 * Note! Callers are responsible for ensuring that the `base` is a suitable match for the given 490 * `refinement`, such that the two can be merged without modifying filter-equality properties. 491 */ mergeRefinementIntoMatchingBaseIntent(Intent base, Intent refinement)492 static Intent mergeRefinementIntoMatchingBaseIntent(Intent base, Intent refinement) { 493 Intent mergedIntent = new Intent(base); 494 495 /* Copy over any fields from the `refinement` that weren't already specified by the `base`, 496 * along with the refined ClipData (if present, even if that overwrites data given in the 497 * `base` intent). 498 * 499 * Refinement may have modified the payload content stored in the ClipData; such changes 500 * are permitted in refinement since ClipData isn't a factor in the determination of 501 * `Intent.filterEquals()` (which must be preserved as an invariant of refinement). */ 502 mergedIntent.fillIn(refinement, Intent.FILL_IN_CLIP_DATA); 503 504 /* Refinement may also modify payload content held in the 'extras' representation, as again 505 * those attributes aren't a factor in determining filter-equality. There is no `FILL_IN_*` 506 * flag that would allow the refinement to overwrite existing keys in the `base` extras, so 507 * here we have to implement the logic ourselves. 508 * 509 * Note this still doesn't imply that the refined intent is the final authority on extras; 510 * in particular, `SelectableTargetInfo.mActivityStarter` uses `Intent.putExtras(Bundle)` to 511 * merge in the `mChooserTargetIntentExtras` (i.e., the `EXTRA_SHORTCUT_ID`), which will 512 * overwrite any existing value. 513 * 514 * TODO: formalize the precedence and make sure extras are set in the appropriate stages, 515 * instead of relying on maintainers to know that (e.g.) authoritative changes belong in the 516 * `TargetActivityStarter`. Otherwise, any extras-based data that Sharesheet adds internally 517 * might be susceptible to "spoofing" from the refinement activity. */ 518 mergedIntent.putExtras(refinement); // Re-merge extras to favor refinement. 519 520 // TODO(b/279067078): consider how to populate the "merged" ClipData. The `base` 521 // already has non-null ClipData due to the implicit migration in Intent, so if the 522 // refinement modified any of the payload extras, they *must* also provide a modified 523 // ClipData, or else the updated "extras" payload will be inconsistent with the 524 // pre-refinement ClipData when they're merged together. We may be able to do better, 525 // but there are complicated tradeoffs. 526 527 return mergedIntent; 528 } 529 } 530