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