1 /** 2 * Copyright (C) 2017 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 package androidx.core.content.pm; 17 18 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX; 19 20 import android.annotation.SuppressLint; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ShortcutInfo; 26 import android.content.pm.ShortcutManager; 27 import android.graphics.drawable.Drawable; 28 import android.net.Uri; 29 import android.os.Build; 30 import android.os.Bundle; 31 import android.os.PersistableBundle; 32 import android.os.UserHandle; 33 import android.text.TextUtils; 34 35 import androidx.annotation.IntDef; 36 import androidx.annotation.RequiresApi; 37 import androidx.annotation.RestrictTo; 38 import androidx.annotation.VisibleForTesting; 39 import androidx.collection.ArraySet; 40 import androidx.core.app.Person; 41 import androidx.core.content.LocusIdCompat; 42 import androidx.core.graphics.drawable.IconCompat; 43 import androidx.core.net.UriCompat; 44 import androidx.core.util.Preconditions; 45 46 import org.jspecify.annotations.NonNull; 47 import org.jspecify.annotations.Nullable; 48 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.HashMap; 54 import java.util.HashSet; 55 import java.util.List; 56 import java.util.Map; 57 import java.util.Set; 58 59 /** 60 * Helper for accessing features in {@link ShortcutInfo}. 61 */ 62 public class ShortcutInfoCompat { 63 64 private static final String EXTRA_PERSON_COUNT = "extraPersonCount"; 65 private static final String EXTRA_PERSON_ = "extraPerson_"; 66 private static final String EXTRA_LOCUS_ID = "extraLocusId"; 67 private static final String EXTRA_LONG_LIVED = "extraLongLived"; 68 69 private static final String EXTRA_SLICE_URI = "extraSliceUri"; 70 71 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 72 @IntDef(flag = true, value = {SURFACE_LAUNCHER}) 73 @Retention(RetentionPolicy.SOURCE) 74 public @interface Surface {} 75 76 /** 77 * Indicates system surfaces managed by a launcher app. e.g. Long-Press Menu. 78 */ 79 public static final int SURFACE_LAUNCHER = 1 << 0; 80 81 Context mContext; 82 String mId; 83 String mPackageName; 84 Intent[] mIntents; 85 ComponentName mActivity; 86 87 CharSequence mLabel; 88 CharSequence mLongLabel; 89 CharSequence mDisabledMessage; 90 91 IconCompat mIcon; 92 boolean mIsAlwaysBadged; 93 94 Person[] mPersons; 95 Set<String> mCategories; 96 97 @Nullable LocusIdCompat mLocusId; 98 // TODO: Support |auto| when the value of mIsLongLived is not set 99 boolean mIsLongLived; 100 101 int mRank; 102 103 PersistableBundle mExtras; 104 Bundle mTransientExtras; 105 106 // Read-Only fields 107 long mLastChangedTimestamp; 108 UserHandle mUser; 109 boolean mIsCached; 110 boolean mIsDynamic; 111 boolean mIsPinned; 112 boolean mIsDeclaredInManifest; 113 boolean mIsImmutable; 114 boolean mIsEnabled = true; 115 boolean mHasKeyFieldsOnly; 116 int mDisabledReason; 117 int mExcludedSurfaces; 118 ShortcutInfoCompat()119 ShortcutInfoCompat() { } 120 121 /** 122 * @return {@link ShortcutInfo} object from this compat object. 123 */ 124 @RequiresApi(25) toShortcutInfo()125 public ShortcutInfo toShortcutInfo() { 126 ShortcutInfo.Builder builder = new ShortcutInfo.Builder(mContext, mId) 127 .setShortLabel(mLabel) 128 .setIntents(mIntents); 129 if (mIcon != null) { 130 builder.setIcon(mIcon.toIcon(mContext)); 131 } 132 if (!TextUtils.isEmpty(mLongLabel)) { 133 builder.setLongLabel(mLongLabel); 134 } 135 if (!TextUtils.isEmpty(mDisabledMessage)) { 136 builder.setDisabledMessage(mDisabledMessage); 137 } 138 if (mActivity != null) { 139 builder.setActivity(mActivity); 140 } 141 if (mCategories != null) { 142 builder.setCategories(mCategories); 143 } 144 builder.setRank(mRank); 145 if (mExtras != null) { 146 builder.setExtras(mExtras); 147 } 148 if (Build.VERSION.SDK_INT >= 29) { 149 if (mPersons != null && mPersons.length > 0) { 150 android.app.Person[] persons = new android.app.Person[mPersons.length]; 151 for (int i = 0; i < persons.length; i++) { 152 persons[i] = mPersons[i].toAndroidPerson(); 153 } 154 builder.setPersons(persons); 155 } 156 if (mLocusId != null) { 157 builder.setLocusId(mLocusId.toLocusId()); 158 } 159 builder.setLongLived(mIsLongLived); 160 } else { 161 // ShortcutInfo.Builder#setPersons(...) and ShortcutInfo.Builder#setLongLived(...) are 162 // introduced in API 29. On older API versions, we store mPersons and mIsLongLived in 163 // the extras field of ShortcutInfo for backwards compatibility. 164 builder.setExtras(buildLegacyExtrasBundle()); 165 } 166 if (Build.VERSION.SDK_INT >= 33) { 167 Api33Impl.setExcludedFromSurfaces(builder, mExcludedSurfaces); 168 } 169 return builder.build(); 170 } 171 172 /** 173 */ 174 @RequiresApi(22) 175 @RestrictTo(LIBRARY_GROUP_PREFIX) buildLegacyExtrasBundle()176 private PersistableBundle buildLegacyExtrasBundle() { 177 if (mExtras == null) { 178 mExtras = new PersistableBundle(); 179 } 180 if (mPersons != null && mPersons.length > 0) { 181 mExtras.putInt(EXTRA_PERSON_COUNT, mPersons.length); 182 for (int i = 0; i < mPersons.length; i++) { 183 mExtras.putPersistableBundle(EXTRA_PERSON_ + (i + 1), 184 mPersons[i].toPersistableBundle()); 185 } 186 } 187 if (mLocusId != null) { 188 mExtras.putString(EXTRA_LOCUS_ID, mLocusId.getId()); 189 } 190 mExtras.putBoolean(EXTRA_LONG_LIVED, mIsLongLived); 191 return mExtras; 192 } 193 addToIntent(Intent outIntent)194 Intent addToIntent(Intent outIntent) { 195 outIntent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, mIntents[mIntents.length - 1]) 196 .putExtra(Intent.EXTRA_SHORTCUT_NAME, mLabel.toString()); 197 if (mIcon != null) { 198 Drawable badge = null; 199 if (mIsAlwaysBadged) { 200 PackageManager pm = mContext.getPackageManager(); 201 if (mActivity != null) { 202 try { 203 badge = pm.getActivityIcon(mActivity); 204 } catch (PackageManager.NameNotFoundException e) { 205 // Ignore 206 } 207 } 208 if (badge == null) { 209 badge = mContext.getApplicationInfo().loadIcon(pm); 210 } 211 } 212 mIcon.addToShortcutIntent(outIntent, badge, mContext); 213 } 214 return outIntent; 215 } 216 217 /** 218 * Returns the ID of a shortcut. 219 * 220 * <p>Shortcut IDs are unique within each publisher app and must be stable across 221 * devices so that shortcuts will still be valid when restored on a different device. 222 * See {@link android.content.pm.ShortcutManager} for details. 223 */ getId()224 public @NonNull String getId() { 225 return mId; 226 } 227 228 /** 229 * Return the package name of the publisher app. 230 */ getPackage()231 public @NonNull String getPackage() { 232 return mPackageName; 233 } 234 235 /** 236 * Return the target activity. 237 * 238 * <p>This has nothing to do with the activity that this shortcut will launch. 239 * Launcher apps should show the launcher icon for the returned activity alongside 240 * this shortcut. 241 * 242 * @see Builder#setActivity(ComponentName) 243 */ getActivity()244 public @Nullable ComponentName getActivity() { 245 return mActivity; 246 } 247 248 /** 249 * Return the short description of a shortcut. 250 * 251 * @see Builder#setShortLabel(CharSequence) 252 */ getShortLabel()253 public @NonNull CharSequence getShortLabel() { 254 return mLabel; 255 } 256 257 /** 258 * Return the long description of a shortcut. 259 * 260 * @see Builder#setLongLabel(CharSequence) 261 */ getLongLabel()262 public @Nullable CharSequence getLongLabel() { 263 return mLongLabel; 264 } 265 266 /** 267 * Return the message that should be shown when the user attempts to start a shortcut 268 * that is disabled. 269 * 270 * @see Builder#setDisabledMessage(CharSequence) 271 */ getDisabledMessage()272 public @Nullable CharSequence getDisabledMessage() { 273 return mDisabledMessage; 274 } 275 276 /** 277 * Returns why a shortcut has been disabled. 278 */ getDisabledReason()279 public int getDisabledReason() { 280 return mDisabledReason; 281 } 282 283 /** 284 * Returns the intent that is executed when the user selects this shortcut. 285 * If setIntents() was used, then return the last intent in the array. 286 * 287 * @see Builder#setIntent(Intent) 288 */ getIntent()289 public @NonNull Intent getIntent() { 290 return mIntents[mIntents.length - 1]; 291 } 292 293 /** 294 * Return the intent set with {@link Builder#setIntents(Intent[])}. 295 * 296 * @see Builder#setIntents(Intent[]) 297 */ getIntents()298 public Intent @NonNull [] getIntents() { 299 return Arrays.copyOf(mIntents, mIntents.length); 300 } 301 302 /** 303 * Return the categories set with {@link Builder#setCategories(Set)}. 304 * 305 * @see Builder#setCategories(Set) 306 */ getCategories()307 public @Nullable Set<String> getCategories() { 308 return mCategories; 309 } 310 311 /** 312 * Gets the {@link LocusIdCompat} associated with this shortcut. 313 * 314 * <p>Used by the device's intelligence services to correlate objects (such as 315 * {@link androidx.core.app.NotificationCompat} and 316 * {@link android.view.contentcapture.ContentCaptureContext}) that are correlated. 317 */ getLocusId()318 public @Nullable LocusIdCompat getLocusId() { 319 return mLocusId; 320 } 321 322 /** 323 * Returns the rank of the shortcut set with {@link Builder#setRank(int)}. 324 * 325 * @see Builder#setRank(int) 326 */ getRank()327 public int getRank() { 328 return mRank; 329 } 330 331 /** 332 */ 333 @RestrictTo(LIBRARY_GROUP_PREFIX) getIcon()334 public IconCompat getIcon() { 335 return mIcon; 336 } 337 338 /** 339 */ 340 @RequiresApi(25) 341 @RestrictTo(LIBRARY_GROUP_PREFIX) 342 @VisibleForTesting getPersonsFromExtra(@onNull PersistableBundle bundle)343 static Person @Nullable [] getPersonsFromExtra(@NonNull PersistableBundle bundle) { 344 if (bundle == null || !bundle.containsKey(EXTRA_PERSON_COUNT)) { 345 return null; 346 } 347 348 int personsLength = bundle.getInt(EXTRA_PERSON_COUNT); 349 Person[] persons = new Person[personsLength]; 350 for (int i = 0; i < personsLength; i++) { 351 persons[i] = Person.fromPersistableBundle( 352 bundle.getPersistableBundle(EXTRA_PERSON_ + (i + 1))); 353 } 354 return persons; 355 } 356 357 /** 358 */ 359 @RequiresApi(25) 360 @RestrictTo(LIBRARY_GROUP_PREFIX) 361 @VisibleForTesting getLongLivedFromExtra(@ullable PersistableBundle bundle)362 static boolean getLongLivedFromExtra(@Nullable PersistableBundle bundle) { 363 if (bundle == null || !bundle.containsKey(EXTRA_LONG_LIVED)) { 364 return false; 365 } 366 return bundle.getBoolean(EXTRA_LONG_LIVED); 367 } 368 369 /** 370 */ 371 @RequiresApi(25) 372 @RestrictTo(LIBRARY_GROUP_PREFIX) fromShortcuts(final @NonNull Context context, final @NonNull List<ShortcutInfo> shortcuts)373 static List<ShortcutInfoCompat> fromShortcuts(final @NonNull Context context, 374 final @NonNull List<ShortcutInfo> shortcuts) { 375 final List<ShortcutInfoCompat> results = new ArrayList<>(shortcuts.size()); 376 for (ShortcutInfo s : shortcuts) { 377 results.add(new ShortcutInfoCompat.Builder(context, s).build()); 378 } 379 return results; 380 } 381 getExtras()382 public @Nullable PersistableBundle getExtras() { 383 return mExtras; 384 } 385 386 /** 387 * Get additional extras from the shortcut, which will not be persisted anywhere once the 388 * shortcut is published. 389 */ 390 @RestrictTo(LIBRARY_GROUP_PREFIX) getTransientExtras()391 public @Nullable Bundle getTransientExtras() { 392 return mTransientExtras; 393 } 394 395 /** 396 * {@link UserHandle} on which the publisher created this shortcut. 397 */ getUserHandle()398 public @Nullable UserHandle getUserHandle() { 399 return mUser; 400 } 401 402 /** 403 * Last time when any of the fields was updated. 404 */ getLastChangedTimestamp()405 public long getLastChangedTimestamp() { 406 return mLastChangedTimestamp; 407 } 408 409 /** Return whether a shortcut is cached. */ isCached()410 public boolean isCached() { 411 return mIsCached; 412 } 413 414 /** Return whether a shortcut is dynamic. */ isDynamic()415 public boolean isDynamic() { 416 return mIsDynamic; 417 } 418 419 /** Return whether a shortcut is pinned. */ isPinned()420 public boolean isPinned() { 421 return mIsPinned; 422 } 423 424 /** 425 * Return whether a shortcut is static; that is, whether a shortcut is 426 * published from AndroidManifest.xml. If {@code true}, the shortcut is 427 * also {@link #isImmutable()}. 428 * 429 * <p>When an app is upgraded and a shortcut is no longer published from AndroidManifest.xml, 430 * this will be set to {@code false}. If the shortcut is not pinned, then it'll disappear. 431 * However, if it's pinned, it will still be visible, {@link #isEnabled()} will be 432 * {@code false} and {@link #isImmutable()} will be {@code true}. 433 */ isDeclaredInManifest()434 public boolean isDeclaredInManifest() { 435 return mIsDeclaredInManifest; 436 } 437 438 /** 439 * Return if a shortcut is immutable, in which case it cannot be modified with any of 440 * {@link ShortcutManagerCompat} APIs. 441 * 442 * <p>All static shortcuts are immutable. When a static shortcut is pinned and is then 443 * disabled because it doesn't appear in AndroidManifest.xml for a newer version of the 444 * app, {@link #isDeclaredInManifest} returns {@code false}, but the shortcut is still 445 * immutable. 446 * 447 * <p>All shortcuts originally published via the {@link ShortcutManager} APIs 448 * are all mutable. 449 */ isImmutable()450 public boolean isImmutable() { 451 return mIsImmutable; 452 } 453 454 /** 455 * Returns {@code false} if a shortcut is disabled with 456 * {@link ShortcutManagerCompat#disableShortcuts}. 457 */ isEnabled()458 public boolean isEnabled() { 459 return mIsEnabled; 460 } 461 462 /** 463 * Return whether a shortcut only contains "key" information only or not. If true, only the 464 * following fields are available. 465 * <ul> 466 * <li>{@link #getId()} 467 * <li>{@link #getPackage()} 468 * <li>{@link #getActivity()} 469 * <li>{@link #getLastChangedTimestamp()} 470 * <li>{@link #isDynamic()} 471 * <li>{@link #isPinned()} 472 * <li>{@link #isDeclaredInManifest()} 473 * <li>{@link #isImmutable()} 474 * <li>{@link #isEnabled()} 475 * <li>{@link #getUserHandle()} 476 * </ul> 477 */ hasKeyFieldsOnly()478 public boolean hasKeyFieldsOnly() { 479 return mHasKeyFieldsOnly; 480 } 481 482 @RequiresApi(25) getLocusId(final @NonNull ShortcutInfo shortcutInfo)483 static @Nullable LocusIdCompat getLocusId(final @NonNull ShortcutInfo shortcutInfo) { 484 if (Build.VERSION.SDK_INT >= 29) { 485 if (shortcutInfo.getLocusId() == null) return null; 486 return LocusIdCompat.toLocusIdCompat(shortcutInfo.getLocusId()); 487 } else { 488 return getLocusIdFromExtra(shortcutInfo.getExtras()); 489 } 490 } 491 492 /** 493 * Return true if the shortcut is excluded from specified surface. 494 */ isExcludedFromSurfaces(@urface int surface)495 public boolean isExcludedFromSurfaces(@Surface int surface) { 496 return (mExcludedSurfaces & surface) != 0; 497 } 498 499 /** 500 * Returns a bitmask of all surfaces this shortcut is excluded from. 501 * 502 * @see ShortcutInfo.Builder#setExcludedFromSurfaces(int) 503 */ 504 @Surface getExcludedFromSurfaces()505 public int getExcludedFromSurfaces() { 506 return mExcludedSurfaces; 507 } 508 509 /** 510 */ 511 @RequiresApi(25) 512 @RestrictTo(LIBRARY_GROUP_PREFIX) getLocusIdFromExtra(@ullable PersistableBundle bundle)513 private static @Nullable LocusIdCompat getLocusIdFromExtra(@Nullable PersistableBundle bundle) { 514 if (bundle == null) return null; 515 final String locusId = bundle.getString(EXTRA_LOCUS_ID); 516 return locusId == null ? null : new LocusIdCompat(locusId); 517 } 518 519 /** 520 * Builder class for {@link ShortcutInfoCompat} objects. 521 */ 522 public static class Builder { 523 524 private final ShortcutInfoCompat mInfo; 525 private boolean mIsConversation; 526 private Set<String> mCapabilityBindings; 527 private Map<String, Map<String, List<String>>> mCapabilityBindingParams; 528 private Uri mSliceUri; 529 Builder(@onNull Context context, @NonNull String id)530 public Builder(@NonNull Context context, @NonNull String id) { 531 mInfo = new ShortcutInfoCompat(); 532 mInfo.mContext = context; 533 mInfo.mId = id; 534 } 535 536 /** 537 */ 538 @RestrictTo(LIBRARY_GROUP_PREFIX) Builder(@onNull ShortcutInfoCompat shortcutInfo)539 public Builder(@NonNull ShortcutInfoCompat shortcutInfo) { 540 mInfo = new ShortcutInfoCompat(); 541 mInfo.mContext = shortcutInfo.mContext; 542 mInfo.mId = shortcutInfo.mId; 543 mInfo.mPackageName = shortcutInfo.mPackageName; 544 mInfo.mIntents = Arrays.copyOf(shortcutInfo.mIntents, shortcutInfo.mIntents.length); 545 mInfo.mActivity = shortcutInfo.mActivity; 546 mInfo.mLabel = shortcutInfo.mLabel; 547 mInfo.mLongLabel = shortcutInfo.mLongLabel; 548 mInfo.mDisabledMessage = shortcutInfo.mDisabledMessage; 549 mInfo.mDisabledReason = shortcutInfo.mDisabledReason; 550 mInfo.mIcon = shortcutInfo.mIcon; 551 mInfo.mIsAlwaysBadged = shortcutInfo.mIsAlwaysBadged; 552 mInfo.mUser = shortcutInfo.mUser; 553 mInfo.mLastChangedTimestamp = shortcutInfo.mLastChangedTimestamp; 554 mInfo.mIsCached = shortcutInfo.mIsCached; 555 mInfo.mIsDynamic = shortcutInfo.mIsDynamic; 556 mInfo.mIsPinned = shortcutInfo.mIsPinned; 557 mInfo.mIsDeclaredInManifest = shortcutInfo.mIsDeclaredInManifest; 558 mInfo.mIsImmutable = shortcutInfo.mIsImmutable; 559 mInfo.mIsEnabled = shortcutInfo.mIsEnabled; 560 mInfo.mLocusId = shortcutInfo.mLocusId; 561 mInfo.mIsLongLived = shortcutInfo.mIsLongLived; 562 mInfo.mHasKeyFieldsOnly = shortcutInfo.mHasKeyFieldsOnly; 563 mInfo.mRank = shortcutInfo.mRank; 564 if (shortcutInfo.mPersons != null) { 565 mInfo.mPersons = Arrays.copyOf(shortcutInfo.mPersons, shortcutInfo.mPersons.length); 566 } 567 if (shortcutInfo.mCategories != null) { 568 mInfo.mCategories = new HashSet<>(shortcutInfo.mCategories); 569 } 570 if (shortcutInfo.mExtras != null) { 571 mInfo.mExtras = shortcutInfo.mExtras; 572 } 573 mInfo.mExcludedSurfaces = shortcutInfo.mExcludedSurfaces; 574 } 575 576 /** 577 */ 578 @RequiresApi(25) 579 @RestrictTo(LIBRARY_GROUP_PREFIX) Builder(@onNull Context context, @NonNull ShortcutInfo shortcutInfo)580 public Builder(@NonNull Context context, @NonNull ShortcutInfo shortcutInfo) { 581 mInfo = new ShortcutInfoCompat(); 582 mInfo.mContext = context; 583 mInfo.mId = shortcutInfo.getId(); 584 mInfo.mPackageName = shortcutInfo.getPackage(); 585 Intent[] intents = shortcutInfo.getIntents(); 586 mInfo.mIntents = Arrays.copyOf(intents, intents.length); 587 mInfo.mActivity = shortcutInfo.getActivity(); 588 mInfo.mLabel = shortcutInfo.getShortLabel(); 589 mInfo.mLongLabel = shortcutInfo.getLongLabel(); 590 mInfo.mDisabledMessage = shortcutInfo.getDisabledMessage(); 591 if (Build.VERSION.SDK_INT >= 28) { 592 mInfo.mDisabledReason = shortcutInfo.getDisabledReason(); 593 } else { 594 mInfo.mDisabledReason = shortcutInfo.isEnabled() 595 ? ShortcutInfo.DISABLED_REASON_NOT_DISABLED 596 : ShortcutInfo.DISABLED_REASON_UNKNOWN; 597 } 598 mInfo.mCategories = shortcutInfo.getCategories(); 599 mInfo.mPersons = ShortcutInfoCompat.getPersonsFromExtra(shortcutInfo.getExtras()); 600 mInfo.mUser = shortcutInfo.getUserHandle(); 601 mInfo.mLastChangedTimestamp = shortcutInfo.getLastChangedTimestamp(); 602 if (Build.VERSION.SDK_INT >= 30) { 603 mInfo.mIsCached = shortcutInfo.isCached(); 604 } 605 mInfo.mIsDynamic = shortcutInfo.isDynamic(); 606 mInfo.mIsPinned = shortcutInfo.isPinned(); 607 mInfo.mIsDeclaredInManifest = shortcutInfo.isDeclaredInManifest(); 608 mInfo.mIsImmutable = shortcutInfo.isImmutable(); 609 mInfo.mIsEnabled = shortcutInfo.isEnabled(); 610 mInfo.mHasKeyFieldsOnly = shortcutInfo.hasKeyFieldsOnly(); 611 mInfo.mLocusId = ShortcutInfoCompat.getLocusId(shortcutInfo); 612 mInfo.mRank = shortcutInfo.getRank(); 613 mInfo.mExtras = shortcutInfo.getExtras(); 614 } 615 616 /** 617 * Sets the short title of a shortcut. 618 * 619 * <p>This is a mandatory field when publishing a new shortcut. 620 * 621 * <p>This field is intended to be a concise description of a shortcut. 622 * 623 * <p>The recommended maximum length is 10 characters. 624 */ setShortLabel(@onNull CharSequence shortLabel)625 public @NonNull Builder setShortLabel(@NonNull CharSequence shortLabel) { 626 mInfo.mLabel = shortLabel; 627 return this; 628 } 629 630 /** 631 * Sets the text of a shortcut. 632 * 633 * <p>This field is intended to be more descriptive than the shortcut title. The launcher 634 * shows this instead of the short title when it has enough space. 635 * 636 * <p>The recommend maximum length is 25 characters. 637 */ setLongLabel(@onNull CharSequence longLabel)638 public @NonNull Builder setLongLabel(@NonNull CharSequence longLabel) { 639 mInfo.mLongLabel = longLabel; 640 return this; 641 } 642 643 /** 644 * Sets the message that should be shown when the user attempts to start a shortcut that 645 * is disabled. 646 * 647 * @see ShortcutInfo#getDisabledMessage() 648 */ setDisabledMessage(@onNull CharSequence disabledMessage)649 public @NonNull Builder setDisabledMessage(@NonNull CharSequence disabledMessage) { 650 mInfo.mDisabledMessage = disabledMessage; 651 return this; 652 } 653 654 /** 655 * Sets the intent of a shortcut. Alternatively, {@link #setIntents(Intent[])} can be used 656 * to launch an activity with other activities in the back stack. 657 * 658 * <p>This is a mandatory field when publishing a new shortcut. 659 * 660 * <p>The given {@code intent} can contain extras, but these extras must contain values 661 * of primitive types in order for the system to persist these values. 662 */ setIntent(@onNull Intent intent)663 public @NonNull Builder setIntent(@NonNull Intent intent) { 664 return setIntents(new Intent[]{intent}); 665 } 666 667 /** 668 * Sets multiple intents instead of a single intent, in order to launch an activity with 669 * other activities in back stack. Use {@link android.app.TaskStackBuilder} to build 670 * intents. The last element in the list represents the only intent that doesn't place 671 * an activity on the back stack. 672 */ setIntents(Intent @onNull [] intents)673 public @NonNull Builder setIntents(Intent @NonNull [] intents) { 674 mInfo.mIntents = intents; 675 return this; 676 } 677 678 /** 679 * Sets an icon of a shortcut. 680 */ setIcon(IconCompat icon)681 public @NonNull Builder setIcon(IconCompat icon) { 682 mInfo.mIcon = icon; 683 return this; 684 } 685 686 /** 687 * Sets the {@link LocusIdCompat} associated with this shortcut. 688 * 689 * <p>This method should be called when the {@link LocusIdCompat} is used in other places 690 * (such as {@link androidx.core.app.NotificationCompat} and 691 * {@link android.view.contentcapture.ContentCaptureContext}) so the device's intelligence 692 * services can correlate them. 693 */ setLocusId(final @Nullable LocusIdCompat locusId)694 public @NonNull Builder setLocusId(final @Nullable LocusIdCompat locusId) { 695 mInfo.mLocusId = locusId; 696 return this; 697 } 698 699 /** 700 * Sets the corresponding fields indicating this shortcut is aimed for conversation. 701 * 702 * <p> 703 * If the shortcut is not associated with a {@link LocusIdCompat}, a {@link LocusIdCompat} 704 * based on {@link ShortcutInfoCompat#getId()} will be added upon {@link #build()} 705 * <p> 706 * Additionally, the shortcut will be long-lived. 707 * @see #setLongLived(boolean) 708 */ setIsConversation()709 public @NonNull Builder setIsConversation() { 710 mIsConversation = true; 711 return this; 712 } 713 714 /** 715 * Sets the target activity. A shortcut will be shown along with this activity's icon 716 * on the launcher. 717 * 718 * @see ShortcutInfo#getActivity() 719 * @see ShortcutInfo.Builder#setActivity(ComponentName) 720 */ setActivity(@onNull ComponentName activity)721 public @NonNull Builder setActivity(@NonNull ComponentName activity) { 722 mInfo.mActivity = activity; 723 return this; 724 } 725 726 /** 727 * Badges the icon before passing it over to the Launcher. 728 * <p> 729 * Launcher automatically badges {@link ShortcutInfo}, so only the legacy shortcut icon, 730 * {@link Intent.ShortcutIconResource} is badged. This field is ignored when using 731 * {@link ShortcutInfo} on API 25 and above. 732 * <p> 733 * If the shortcut is associated with an activity, the activity icon is used as the badge, 734 * otherwise application icon is used. 735 * 736 * @see #setActivity(ComponentName) 737 */ setAlwaysBadged()738 public @NonNull Builder setAlwaysBadged() { 739 mInfo.mIsAlwaysBadged = true; 740 return this; 741 } 742 743 /** 744 * Associate a person to a shortcut. Alternatively, {@link #setPersons(Person[])} can be 745 * used to add multiple persons to a shortcut. 746 * 747 * <p>This is an optional field when publishing a new shortcut. 748 * 749 * @see Person 750 */ setPerson(@onNull Person person)751 public @NonNull Builder setPerson(@NonNull Person person) { 752 return setPersons(new Person[]{person}); 753 } 754 755 /** 756 * Sets multiple persons instead of a single person. 757 */ setPersons(Person @onNull [] persons)758 public @NonNull Builder setPersons(Person @NonNull [] persons) { 759 mInfo.mPersons = persons; 760 return this; 761 } 762 763 /** 764 * Sets categories for a shortcut. 765 * <ul> 766 * <li>Launcher apps may use this information to categorize shortcuts 767 * <li> Used by the system to associate a published Sharing Shortcut with supported 768 * mimeTypes. Required for published Sharing Shortcuts with a matching category 769 * declared in share targets, defined in the app's manifest linked shortcuts xml file. 770 * </ul> 771 * 772 * @see ShortcutInfo#getCategories() 773 */ setCategories(@onNull Set<String> categories)774 public @NonNull Builder setCategories(@NonNull Set<String> categories) { 775 ArraySet<String> set = new ArraySet<>(); 776 set.addAll(categories); 777 mInfo.mCategories = set; 778 return this; 779 } 780 781 /** 782 * @deprecated Use {@ink #setLongLived(boolean)) instead. 783 */ 784 @Deprecated setLongLived()785 public @NonNull Builder setLongLived() { 786 mInfo.mIsLongLived = true; 787 return this; 788 } 789 790 /** 791 * Sets if a shortcut would be valid even if it has been unpublished/invisible by the app 792 * (as a dynamic or pinned shortcut). If it is long lived, it can be cached by various 793 * system services even after it has been unpublished as a dynamic shortcut. 794 */ setLongLived(boolean longLived)795 public @NonNull Builder setLongLived(boolean longLived) { 796 mInfo.mIsLongLived = longLived; 797 return this; 798 } 799 800 /** 801 * Sets which surfaces a shortcut will be excluded from. 802 * 803 * This API is reserved for future extension. Currently, marking a shortcut to be 804 * excluded from {@link #SURFACE_LAUNCHER} will not publish the shortcut, thus 805 * the following operations will be a no-op: 806 * {@link android.content.pm.ShortcutManager#pushDynamicShortcut(android.content.pm.ShortcutInfo)}, 807 * {@link android.content.pm.ShortcutManager#addDynamicShortcuts(List)}, and 808 * {@link android.content.pm.ShortcutManager#setDynamicShortcuts(List)}. 809 * 810 * <p>On API <= 31, shortcuts that are excluded from {@link #SURFACE_LAUNCHER} are not 811 * actually sent to {@link ShortcutManager}. These shortcuts might still be made 812 * available to other surfaces via alternative means. 813 */ setExcludedFromSurfaces(final int surfaces)814 public @NonNull Builder setExcludedFromSurfaces(final int surfaces) { 815 mInfo.mExcludedSurfaces = surfaces; 816 return this; 817 } 818 819 /** 820 * Sets rank of a shortcut, which is a non-negative value that's used by the system to sort 821 * shortcuts. Lower value means higher importance. 822 * 823 * @see ShortcutInfo#getRank() for details. 824 */ setRank(int rank)825 public @NonNull Builder setRank(int rank) { 826 mInfo.mRank = rank; 827 return this; 828 } 829 830 /** 831 * Extras that the app can set for any purpose. 832 * 833 * <p>Apps can store arbitrary shortcut metadata in extras and retrieve the 834 * metadata later using {@link ShortcutInfo#getExtras()}. 835 * 836 * @see ShortcutInfo#getExtras 837 */ setExtras(@onNull PersistableBundle extras)838 public @NonNull Builder setExtras(@NonNull PersistableBundle extras) { 839 mInfo.mExtras = extras; 840 return this; 841 } 842 843 /** 844 */ 845 @RestrictTo(LIBRARY_GROUP_PREFIX) setTransientExtras(final @NonNull Bundle transientExtras)846 public @NonNull Builder setTransientExtras(final @NonNull Bundle transientExtras) { 847 mInfo.mTransientExtras = Preconditions.checkNotNull(transientExtras); 848 return this; 849 } 850 851 /** 852 * Associates a shortcut with a capability without any parameters. Used when the shortcut is 853 * an instance of a capability. 854 * 855 * <P>This method can be called multiple times to associate multiple capabilities with 856 * this shortcut. 857 * 858 * @param capability capability associated with the shortcut. e.g. actions.intent 859 * .START_EXERCISE. 860 */ 861 @SuppressLint("MissingGetterMatchingBuilder") addCapabilityBinding(@onNull String capability)862 public @NonNull Builder addCapabilityBinding(@NonNull String capability) { 863 if (mCapabilityBindings == null) { 864 mCapabilityBindings = new HashSet<>(); 865 } 866 mCapabilityBindings.add(capability); 867 return this; 868 } 869 870 /** 871 * Associates a shortcut with a capability, and a parameter of that capability. Used when 872 * the shortcut is an instance of a capability. 873 * 874 * <P>This method can be called multiple times to associate multiple capabilities with 875 * this shortcut, or add multiple parameters to the same capability. 876 * 877 * @param capability capability associated with the shortcut. e.g. actions.intent 878 * .START_EXERCISE. 879 * @param parameter the parameter associated with the capability. e.g. exercise.name. 880 * @param parameterValues a list of values for that parameters. The first value will be 881 * the primary name, while the rest will be alternative names. If 882 * the values are empty, then the parameter will not be saved in 883 * the shortcut. 884 */ 885 @SuppressLint("MissingGetterMatchingBuilder") addCapabilityBinding(@onNull String capability, @NonNull String parameter, @NonNull List<String> parameterValues)886 public @NonNull Builder addCapabilityBinding(@NonNull String capability, 887 @NonNull String parameter, @NonNull List<String> parameterValues) { 888 addCapabilityBinding(capability); 889 890 if (!parameterValues.isEmpty()) { 891 if (mCapabilityBindingParams == null) { 892 mCapabilityBindingParams = new HashMap<>(); 893 } 894 if (mCapabilityBindingParams.get(capability) == null) { 895 mCapabilityBindingParams.put(capability, new HashMap<String, List<String>>()); 896 } 897 898 mCapabilityBindingParams.get(capability).put(parameter, parameterValues); 899 } 900 return this; 901 } 902 903 /** 904 * Sets the slice uri for a shortcut. The uri will be used if this shortcuts represents a 905 * slice, instead of an intent. 906 */ 907 @SuppressLint("MissingGetterMatchingBuilder") setSliceUri(@onNull Uri sliceUri)908 public @NonNull Builder setSliceUri(@NonNull Uri sliceUri) { 909 mSliceUri = sliceUri; 910 return this; 911 } 912 913 /** 914 * Creates a {@link ShortcutInfoCompat} instance. 915 */ build()916 public @NonNull ShortcutInfoCompat build() { 917 // Verify the arguments 918 if (TextUtils.isEmpty(mInfo.mLabel)) { 919 throw new IllegalArgumentException("Shortcut must have a non-empty label"); 920 } 921 if (mInfo.mIntents == null || mInfo.mIntents.length == 0) { 922 throw new IllegalArgumentException("Shortcut must have an intent"); 923 } 924 if (mIsConversation) { 925 if (mInfo.mLocusId == null) { 926 mInfo.mLocusId = new LocusIdCompat(mInfo.mId); 927 } 928 mInfo.mIsLongLived = true; 929 } 930 931 if (mCapabilityBindings != null) { 932 if (mInfo.mCategories == null) { 933 mInfo.mCategories = new HashSet<>(); 934 } 935 mInfo.mCategories.addAll(mCapabilityBindings); 936 } 937 if (Build.VERSION.SDK_INT >= 21) { 938 if (mCapabilityBindingParams != null) { 939 if (mInfo.mExtras == null) { 940 mInfo.mExtras = new PersistableBundle(); 941 } 942 for (String capability : mCapabilityBindingParams.keySet()) { 943 final Map<String, List<String>> params = 944 mCapabilityBindingParams.get(capability); 945 final Set<String> paramNames = params.keySet(); 946 // Persist the mapping of <Capability1> -> [<Param1>, <Param2> ... ] 947 mInfo.mExtras.putStringArray( 948 capability, paramNames.toArray(new String[0])); 949 // Persist the capability param in respect to capability 950 // i.e. <Capability1/Param1> -> [<Value1>, <Value2> ... ] 951 for (String paramName : params.keySet()) { 952 final List<String> value = params.get(paramName); 953 mInfo.mExtras.putStringArray(capability + "/" + paramName, 954 value == null ? new String[0] : value.toArray(new String[0])); 955 } 956 } 957 } 958 if (mSliceUri != null) { 959 if (mInfo.mExtras == null) { 960 mInfo.mExtras = new PersistableBundle(); 961 } 962 mInfo.mExtras.putString(EXTRA_SLICE_URI, UriCompat.toSafeString(mSliceUri)); 963 } 964 } 965 return mInfo; 966 } 967 } 968 969 @RequiresApi(33) 970 private static class Api33Impl { setExcludedFromSurfaces(final ShortcutInfo.@NonNull Builder builder, final int surfaces)971 static void setExcludedFromSurfaces(final ShortcutInfo.@NonNull Builder builder, 972 final int surfaces) { 973 builder.setExcludedFromSurfaces(surfaces); 974 } 975 } 976 } 977