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 import android.app.Activity; 20 import android.app.prediction.AppTarget; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ResolveInfo; 25 import android.content.pm.ShortcutInfo; 26 import android.graphics.drawable.Icon; 27 import android.os.Bundle; 28 import android.os.UserHandle; 29 import android.provider.DeviceConfig; 30 import android.service.chooser.ChooserTarget; 31 import android.text.SpannableStringBuilder; 32 import android.util.HashedStringCache; 33 import android.util.Log; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Live target, currently selectable by the user. 44 * @see NotSelectableTargetInfo 45 */ 46 public final class SelectableTargetInfo extends ChooserTargetInfo { 47 private static final String TAG = "SelectableTargetInfo"; 48 49 private interface TargetHashProvider { getHashedTargetIdForMetrics(Context context)50 HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context); 51 } 52 53 private interface TargetActivityStarter { start(Activity activity, Bundle options)54 boolean start(Activity activity, Bundle options); startAsCaller(Activity activity, Bundle options, int userId)55 boolean startAsCaller(Activity activity, Bundle options, int userId); startAsUser(Activity activity, Bundle options, UserHandle user)56 boolean startAsUser(Activity activity, Bundle options, UserHandle user); 57 } 58 59 private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity"; // For legacy reasons. 60 private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7; 61 62 private final int mMaxHashSaltDays = DeviceConfig.getInt( 63 DeviceConfig.NAMESPACE_SYSTEMUI, 64 SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS, 65 DEFAULT_SALT_EXPIRATION_DAYS); 66 67 @Nullable 68 private final DisplayResolveInfo mSourceInfo; 69 @Nullable 70 private final ResolveInfo mBackupResolveInfo; 71 private final Intent mResolvedIntent; 72 private final String mDisplayLabel; 73 @Nullable 74 private final AppTarget mAppTarget; 75 @Nullable 76 private final ShortcutInfo mShortcutInfo; 77 78 private final ComponentName mChooserTargetComponentName; 79 private final CharSequence mChooserTargetUnsanitizedTitle; 80 private final Icon mChooserTargetIcon; 81 private final Bundle mChooserTargetIntentExtras; 82 private final boolean mIsPinned; 83 private final float mModifiedScore; 84 private final boolean mIsSuspended; 85 private final ComponentName mResolvedComponentName; 86 private final Intent mBaseIntentToSend; 87 private final ResolveInfo mResolveInfo; 88 private final List<Intent> mAllSourceIntents; 89 private final IconHolder mDisplayIconHolder = new SettableIconHolder(); 90 private final TargetHashProvider mHashProvider; 91 private final TargetActivityStarter mActivityStarter; 92 93 /** 94 * An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null}) 95 * in its extended data under the key {@link Intent#EXTRA_REFERRER}. 96 */ 97 private final Intent mReferrerFillInIntent; 98 99 /** 100 * Create a new {@link TargetInfo} instance representing a selectable target. Some target 101 * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure. 102 * 103 * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}. 104 */ 105 @Deprecated newSelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, ChooserTarget chooserTarget, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)106 public static TargetInfo newSelectableTargetInfo( 107 @Nullable DisplayResolveInfo sourceInfo, 108 @Nullable ResolveInfo backupResolveInfo, 109 Intent resolvedIntent, 110 ChooserTarget chooserTarget, 111 float modifiedScore, 112 @Nullable ShortcutInfo shortcutInfo, 113 @Nullable AppTarget appTarget, 114 Intent referrerFillInIntent) { 115 return newSelectableTargetInfo( 116 sourceInfo, 117 backupResolveInfo, 118 resolvedIntent, 119 chooserTarget.getComponentName(), 120 chooserTarget.getTitle(), 121 chooserTarget.getIcon(), 122 chooserTarget.getIntentExtras(), 123 modifiedScore, 124 shortcutInfo, 125 appTarget, 126 referrerFillInIntent); 127 } 128 129 /** 130 * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*` 131 * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures 132 * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed 133 * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly 134 * as if they had been provided in the legacy representation. 135 * 136 * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename 137 * to avoid making reference to the legacy type; and reflect the improved semantics in the 138 * signature (and documentation) of this method. 139 */ newSelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, ComponentName chooserTargetComponentName, CharSequence chooserTargetUnsanitizedTitle, Icon chooserTargetIcon, @Nullable Bundle chooserTargetIntentExtras, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)140 public static TargetInfo newSelectableTargetInfo( 141 @Nullable DisplayResolveInfo sourceInfo, 142 @Nullable ResolveInfo backupResolveInfo, 143 Intent resolvedIntent, 144 ComponentName chooserTargetComponentName, 145 CharSequence chooserTargetUnsanitizedTitle, 146 Icon chooserTargetIcon, 147 @Nullable Bundle chooserTargetIntentExtras, 148 float modifiedScore, 149 @Nullable ShortcutInfo shortcutInfo, 150 @Nullable AppTarget appTarget, 151 Intent referrerFillInIntent) { 152 return new SelectableTargetInfo( 153 sourceInfo, 154 backupResolveInfo, 155 resolvedIntent, 156 null, 157 chooserTargetComponentName, 158 chooserTargetUnsanitizedTitle, 159 chooserTargetIcon, 160 chooserTargetIntentExtras, 161 modifiedScore, 162 shortcutInfo, 163 appTarget, 164 referrerFillInIntent); 165 } 166 SelectableTargetInfo( @ullable DisplayResolveInfo sourceInfo, @Nullable ResolveInfo backupResolveInfo, Intent resolvedIntent, @Nullable Intent baseIntentToSend, ComponentName chooserTargetComponentName, CharSequence chooserTargetUnsanitizedTitle, Icon chooserTargetIcon, Bundle chooserTargetIntentExtras, float modifiedScore, @Nullable ShortcutInfo shortcutInfo, @Nullable AppTarget appTarget, Intent referrerFillInIntent)167 private SelectableTargetInfo( 168 @Nullable DisplayResolveInfo sourceInfo, 169 @Nullable ResolveInfo backupResolveInfo, 170 Intent resolvedIntent, 171 @Nullable Intent baseIntentToSend, 172 ComponentName chooserTargetComponentName, 173 CharSequence chooserTargetUnsanitizedTitle, 174 Icon chooserTargetIcon, 175 Bundle chooserTargetIntentExtras, 176 float modifiedScore, 177 @Nullable ShortcutInfo shortcutInfo, 178 @Nullable AppTarget appTarget, 179 Intent referrerFillInIntent) { 180 mSourceInfo = sourceInfo; 181 mBackupResolveInfo = backupResolveInfo; 182 mResolvedIntent = resolvedIntent; 183 mModifiedScore = modifiedScore; 184 mShortcutInfo = shortcutInfo; 185 mAppTarget = appTarget; 186 mReferrerFillInIntent = referrerFillInIntent; 187 mChooserTargetComponentName = chooserTargetComponentName; 188 mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle; 189 mChooserTargetIcon = chooserTargetIcon; 190 mChooserTargetIntentExtras = chooserTargetIntentExtras; 191 192 mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned(); 193 mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle); 194 mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended(); 195 mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; 196 197 mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo); 198 199 mBaseIntentToSend = getBaseIntentToSend( 200 baseIntentToSend, 201 mResolvedIntent, 202 mReferrerFillInIntent); 203 204 mAllSourceIntents = getAllSourceIntents(sourceInfo, mBaseIntentToSend); 205 206 mHashProvider = context -> { 207 final String plaintext = 208 getChooserTargetComponentName().getPackageName() 209 + mChooserTargetUnsanitizedTitle; 210 return HashedStringCache.getInstance().hashString( 211 context, 212 HASHED_STRING_CACHE_TAG, 213 plaintext, 214 mMaxHashSaltDays); 215 }; 216 217 mActivityStarter = new TargetActivityStarter() { 218 @Override 219 public boolean start(Activity activity, Bundle options) { 220 throw new RuntimeException("ChooserTargets should be started as caller."); 221 } 222 223 @Override 224 public boolean startAsCaller(Activity activity, Bundle options, int userId) { 225 final Intent intent = mBaseIntentToSend; 226 if (intent == null) { 227 return false; 228 } 229 intent.setComponent(getChooserTargetComponentName()); 230 intent.putExtras(mChooserTargetIntentExtras); 231 TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId); 232 233 // Important: we will ignore the target security checks in ActivityManager if and 234 // only if the ChooserTarget's target package is the same package where we got the 235 // ChooserTargetService that provided it. This lets a ChooserTargetService provide 236 // a non-exported or permission-guarded target for the user to pick. 237 // 238 // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere 239 // so we'll obey the caller's normal security checks. 240 final boolean ignoreTargetSecurity = (mSourceInfo != null) 241 && mSourceInfo.getResolvedComponentName().getPackageName() 242 .equals(getChooserTargetComponentName().getPackageName()); 243 activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId); 244 return true; 245 } 246 247 @Override 248 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 249 throw new RuntimeException("ChooserTargets should be started as caller."); 250 } 251 }; 252 } 253 SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend)254 private SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend) { 255 this( 256 other.mSourceInfo, 257 other.mBackupResolveInfo, 258 other.mResolvedIntent, 259 baseIntentToSend, 260 other.mChooserTargetComponentName, 261 other.mChooserTargetUnsanitizedTitle, 262 other.mChooserTargetIcon, 263 other.mChooserTargetIntentExtras, 264 other.mModifiedScore, 265 other.mShortcutInfo, 266 other.mAppTarget, 267 other.mReferrerFillInIntent); 268 } 269 270 @Override 271 @Nullable tryToCloneWithAppliedRefinement(Intent proposedRefinement)272 public TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) { 273 Intent matchingBase = 274 getAllSourceIntents() 275 .stream() 276 .filter(i -> i.filterEquals(proposedRefinement)) 277 .findFirst() 278 .orElse(null); 279 if (matchingBase == null) { 280 return null; 281 } 282 283 return new SelectableTargetInfo( 284 this, 285 TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement)); 286 } 287 288 @Override getHashedTargetIdForMetrics(Context context)289 public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) { 290 return mHashProvider.getHashedTargetIdForMetrics(context); 291 } 292 293 @Override isSelectableTargetInfo()294 public boolean isSelectableTargetInfo() { 295 return true; 296 } 297 298 @Override isSuspended()299 public boolean isSuspended() { 300 return mIsSuspended; 301 } 302 303 @Override 304 @Nullable getDisplayResolveInfo()305 public DisplayResolveInfo getDisplayResolveInfo() { 306 return mSourceInfo; 307 } 308 309 @Override getModifiedScore()310 public float getModifiedScore() { 311 return mModifiedScore; 312 } 313 314 @Override getResolvedIntent()315 public Intent getResolvedIntent() { 316 return mResolvedIntent; 317 } 318 319 @Override getResolvedComponentName()320 public ComponentName getResolvedComponentName() { 321 return mResolvedComponentName; 322 } 323 324 @Override getChooserTargetComponentName()325 public ComponentName getChooserTargetComponentName() { 326 return mChooserTargetComponentName; 327 } 328 329 @Nullable getChooserTargetIcon()330 public Icon getChooserTargetIcon() { 331 return mChooserTargetIcon; 332 } 333 334 @Override startAsCaller(Activity activity, Bundle options, int userId)335 public boolean startAsCaller(Activity activity, Bundle options, int userId) { 336 return mActivityStarter.startAsCaller(activity, options, userId); 337 } 338 339 @Override startAsUser(Activity activity, Bundle options, UserHandle user)340 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 341 return mActivityStarter.startAsUser(activity, options, user); 342 } 343 344 @Nullable 345 @Override getTargetIntent()346 public Intent getTargetIntent() { 347 return mBaseIntentToSend; 348 } 349 350 @Override getResolveInfo()351 public ResolveInfo getResolveInfo() { 352 return mResolveInfo; 353 } 354 355 @Override getDisplayLabel()356 public CharSequence getDisplayLabel() { 357 return mDisplayLabel; 358 } 359 360 @Override getExtendedInfo()361 public CharSequence getExtendedInfo() { 362 // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. 363 return null; 364 } 365 366 @Override getDisplayIconHolder()367 public IconHolder getDisplayIconHolder() { 368 return mDisplayIconHolder; 369 } 370 371 @Override 372 @Nullable getDirectShareShortcutInfo()373 public ShortcutInfo getDirectShareShortcutInfo() { 374 return mShortcutInfo; 375 } 376 377 @Override 378 @Nullable getDirectShareAppTarget()379 public AppTarget getDirectShareAppTarget() { 380 return mAppTarget; 381 } 382 383 @Override getAllSourceIntents()384 public List<Intent> getAllSourceIntents() { 385 return mAllSourceIntents; 386 } 387 388 @Override isPinned()389 public boolean isPinned() { 390 return mIsPinned; 391 } 392 sanitizeDisplayLabel(CharSequence label)393 private static String sanitizeDisplayLabel(CharSequence label) { 394 SpannableStringBuilder sb = new SpannableStringBuilder(label); 395 sb.clearSpans(); 396 return sb.toString(); 397 } 398 getAllSourceIntents( @ullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent)399 private static List<Intent> getAllSourceIntents( 400 @Nullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent) { 401 final List<Intent> results = new ArrayList<>(); 402 if (sourceInfo != null) { 403 results.addAll(sourceInfo.getAllSourceIntents()); 404 } else { 405 // This target wasn't joined to a `DisplayResolveInfo` result from our intent-resolution 406 // step, so it was provided directly by the caller. We don't support alternate intents 407 // in this case, but we still permit refinement of the intent we'll dispatch; e.g., 408 // clients may use this hook to defer the computation of "lazy" extras in their share 409 // payload. Note this accommodation isn't strictly "necessary" because clients could 410 // always implement equivalent behavior by pointing custom targets back at their own app 411 // for any amount of further refinement/modification outside of the Sharesheet flow; 412 // nevertheless, it's offered as a convenience for clients who may expect their normal 413 // refinement logic to apply equally in the case of these "special targets." 414 results.add(fallbackSourceIntent); 415 } 416 return results; 417 } 418 getResolvedComponentName( @ullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo)419 private static ComponentName getResolvedComponentName( 420 @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) { 421 if (sourceInfo != null) { 422 return sourceInfo.getResolvedComponentName(); 423 } else if (backupResolveInfo != null) { 424 return new ComponentName( 425 backupResolveInfo.activityInfo.packageName, 426 backupResolveInfo.activityInfo.name); 427 } 428 return null; 429 } 430 431 @Nullable getBaseIntentToSend( @ullable Intent providedBase, @Nullable Intent fallbackBase, Intent referrerFillInIntent)432 private static Intent getBaseIntentToSend( 433 @Nullable Intent providedBase, 434 @Nullable Intent fallbackBase, 435 Intent referrerFillInIntent) { 436 Intent result = (providedBase != null) ? providedBase : fallbackBase; 437 if (result == null) { 438 Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); 439 } else { 440 result = new Intent(result); 441 result.fillIn(referrerFillInIntent, 0); 442 } 443 return result; 444 } 445 } 446