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