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 
17 package androidx.core.content.pm;
18 
19 import static androidx.core.graphics.drawable.IconCompat.TYPE_URI;
20 import static androidx.core.graphics.drawable.IconCompat.TYPE_URI_ADAPTIVE_BITMAP;
21 
22 import android.app.Activity;
23 import android.app.ActivityManager;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentSender;
28 import android.content.pm.ActivityInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.content.pm.ShortcutInfo;
32 import android.content.pm.ShortcutManager;
33 import android.graphics.Bitmap;
34 import android.graphics.BitmapFactory;
35 import android.os.Build;
36 import android.os.Bundle;
37 import android.text.TextUtils;
38 import android.util.DisplayMetrics;
39 
40 import androidx.annotation.IntDef;
41 import androidx.annotation.RequiresApi;
42 import androidx.annotation.RestrictTo;
43 import androidx.annotation.RestrictTo.Scope;
44 import androidx.annotation.VisibleForTesting;
45 import androidx.core.content.ContextCompat;
46 import androidx.core.graphics.drawable.IconCompat;
47 import androidx.core.util.Preconditions;
48 
49 import org.jspecify.annotations.NonNull;
50 import org.jspecify.annotations.Nullable;
51 
52 import java.io.InputStream;
53 import java.lang.annotation.Retention;
54 import java.lang.annotation.RetentionPolicy;
55 import java.lang.reflect.Method;
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.Collections;
59 import java.util.List;
60 import java.util.Objects;
61 
62 /**
63  * Helper for accessing features in {@link android.content.pm.ShortcutManager}.
64  */
65 public class ShortcutManagerCompat {
66 
67     /**
68      * Include manifest shortcuts in the result.
69      *
70      * @see #getShortcuts
71      */
72     public static final int FLAG_MATCH_MANIFEST = 1 << 0;
73 
74     /**
75      * Include dynamic shortcuts in the result.
76      *
77      * @see #getShortcuts
78      */
79     public static final int FLAG_MATCH_DYNAMIC = 1 << 1;
80 
81     /**
82      * Include pinned shortcuts in the result.
83      *
84      * @see #getShortcuts
85      */
86     public static final int FLAG_MATCH_PINNED = 1 << 2;
87 
88     /**
89      * Include cached shortcuts in the result.
90      *
91      * @see #getShortcuts
92      */
93     public static final int FLAG_MATCH_CACHED = 1 << 3;
94 
95     @RestrictTo(Scope.LIBRARY_GROUP_PREFIX)
96     @IntDef(flag = true, value = {
97             FLAG_MATCH_MANIFEST,
98             FLAG_MATCH_DYNAMIC,
99             FLAG_MATCH_PINNED,
100             FLAG_MATCH_CACHED,
101     })
102     @Retention(RetentionPolicy.SOURCE)
103     public @interface ShortcutMatchFlags {}
104 
105     @VisibleForTesting static final String ACTION_INSTALL_SHORTCUT =
106             "com.android.launcher.action.INSTALL_SHORTCUT";
107     @VisibleForTesting static final String INSTALL_SHORTCUT_PERMISSION =
108             "com.android.launcher.permission.INSTALL_SHORTCUT";
109 
110     private static final int DEFAULT_MAX_ICON_DIMENSION_DP = 96;
111     private static final int DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP = 48;
112 
113     /**
114      * Key to get the shortcut ID from extras of a share intent.
115      *
116      * When user selects a direct share item from ShareSheet, the app will receive a share intent
117      * which includes the ID of the corresponding shortcut in the extras field.
118      */
119     public static final String EXTRA_SHORTCUT_ID = "android.intent.extra.shortcut.ID";
120 
121     /**
122      * ShortcutInfoCompatSaver instance that provides APIs to persist shortcuts locally.
123      *
124      * Will be instantiated by reflection to load an implementation from another module if possible.
125      * If fails to load an implementation via reflection, will use the default implementation which
126      * is no-op to avoid unnecessary disk I/O.
127      */
128     private static volatile ShortcutInfoCompatSaver<?> sShortcutInfoCompatSaver = null;
129 
130     /**
131      * Will be instantiated by reflection to load an implementation from another module if
132      * possible. Modules can declare the class to be instantiated using the meta-data in their
133      * Manifest.
134      *
135      * If fails to load an implementation via reflection, will use the default implementation which
136      * is no-op.
137      */
138     private static volatile List<ShortcutInfoChangeListener> sShortcutInfoChangeListeners = null;
139 
140     private static final String SHORTCUT_LISTENER_INTENT_FILTER_ACTION = "androidx.core.content.pm"
141             + ".SHORTCUT_LISTENER";
142     private static final String SHORTCUT_LISTENER_META_DATA_KEY = "androidx.core.content.pm"
143             + ".shortcut_listener_impl";
144 
ShortcutManagerCompat()145     private ShortcutManagerCompat() {
146         /* Hide constructor */
147     }
148 
149     /**
150      * @return {@code true} if the launcher supports {@link #requestPinShortcut},
151      * {@code false} otherwise
152      */
isRequestPinShortcutSupported(@onNull Context context)153     public static boolean isRequestPinShortcutSupported(@NonNull Context context) {
154         if (Build.VERSION.SDK_INT >= 26) {
155             return context.getSystemService(ShortcutManager.class).isRequestPinShortcutSupported();
156         }
157 
158         if (ContextCompat.checkSelfPermission(context, INSTALL_SHORTCUT_PERMISSION)
159                 != PackageManager.PERMISSION_GRANTED) {
160             return false;
161         }
162         for (ResolveInfo info : context.getPackageManager().queryBroadcastReceivers(
163                 new Intent(ACTION_INSTALL_SHORTCUT), 0)) {
164             String permission = info.activityInfo.permission;
165             if (TextUtils.isEmpty(permission) || INSTALL_SHORTCUT_PERMISSION.equals(permission)) {
166                 return true;
167             }
168         }
169         return false;
170     }
171 
172     /**
173      * Request to create a pinned shortcut.
174      * <p>On API <= 25 it creates a legacy shortcut with the provided icon, label and intent. For
175      * newer APIs it will create a {@link android.content.pm.ShortcutInfo} object which can be
176      * updated by the app.
177      *
178      * <p>Use {@link android.app.PendingIntent#getIntentSender()} to create a {@link IntentSender}.
179      *
180      * @param context context to use for the request.
181      * @param shortcut new shortcut to pin
182      * @param callback if not null, this intent will be sent when the shortcut is pinned
183      *
184      * @return {@code true} if the launcher supports this feature
185      *
186      * @see #isRequestPinShortcutSupported
187      * @see IntentSender
188      * @see android.app.PendingIntent#getIntentSender()
189      */
requestPinShortcut(final @NonNull Context context, @NonNull ShortcutInfoCompat shortcut, final @Nullable IntentSender callback)190     public static boolean requestPinShortcut(final @NonNull Context context,
191             @NonNull ShortcutInfoCompat shortcut, final @Nullable IntentSender callback) {
192         if (Build.VERSION.SDK_INT <= 32
193                 && shortcut.isExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)) {
194             // A shortcut that is not frequently used cannot be pinned to WorkSpace.
195             return false;
196         }
197         if (Build.VERSION.SDK_INT >= 26) {
198             return context.getSystemService(ShortcutManager.class).requestPinShortcut(
199                     shortcut.toShortcutInfo(), callback);
200         }
201 
202         if (!isRequestPinShortcutSupported(context)) {
203             return false;
204         }
205         Intent intent = shortcut.addToIntent(new Intent(ACTION_INSTALL_SHORTCUT));
206 
207         // If the callback is null, just send the broadcast
208         if (callback == null) {
209             context.sendBroadcast(intent);
210             return true;
211         }
212 
213         // Otherwise send the callback when the intent has successfully been dispatched.
214         context.sendOrderedBroadcast(intent, null, new BroadcastReceiver() {
215             @Override
216             public void onReceive(Context context, Intent intent) {
217                 try {
218                     callback.sendIntent(context, 0, null, null, null);
219                 } catch (IntentSender.SendIntentException e) {
220                     // Ignore
221                 }
222             }
223         }, null, Activity.RESULT_OK, null, null);
224         return true;
225     }
226 
227     /**
228      * Returns an Intent which can be used by the launcher to pin shortcut.
229      * <p>This should be used by an Activity to set result in response to
230      * {@link Intent#ACTION_CREATE_SHORTCUT}.
231      *
232      * @param context context to use for the intent.
233      * @param shortcut new shortcut to pin
234      * @return the intent that should be set as the result for the calling activity
235      *
236      * @see Intent#ACTION_CREATE_SHORTCUT
237      */
createShortcutResultIntent(@onNull Context context, @NonNull ShortcutInfoCompat shortcut)238     public static @NonNull Intent createShortcutResultIntent(@NonNull Context context,
239             @NonNull ShortcutInfoCompat shortcut) {
240         Intent result = null;
241         if (Build.VERSION.SDK_INT >= 26) {
242             result = context.getSystemService(ShortcutManager.class)
243                     .createShortcutResultIntent(shortcut.toShortcutInfo());
244         }
245         if (result == null) {
246             result = new Intent();
247         }
248         return shortcut.addToIntent(result);
249     }
250 
251     /**
252      * Returns {@link ShortcutInfoCompat}s that match {@code matchFlags}.
253      *
254      * @param matchFlags result includes shortcuts matching this flags. Any combination of:
255      * <ul>
256      *     <li>{@link #FLAG_MATCH_MANIFEST}
257      *     <li>{@link #FLAG_MATCH_DYNAMIC}
258      *     <li>{@link #FLAG_MATCH_PINNED}
259      *     <li>{@link #FLAG_MATCH_CACHED}
260      * </ul>
261      *
262      * Compatibility behavior:
263      * <ul>
264      *      <li>API 30 and above, this method matches platform behavior.
265      *      <li>API 25 through 29, this method aggregates the result from corresponding platform
266      *                   api.
267      *      <li>API 24 and earlier, this method can only returns dynamic shortcut. Calling this
268      *                   method with other flag will be ignored.
269      * </ul>
270      *
271      * @param context context to use for the shortcuts.
272      * @return list of {@link ShortcutInfoCompat}s that match the flag.
273      *
274      * <p>At least one of the {@code MATCH} flags should be set. Otherwise no shortcuts will be
275      * returned.
276      *
277      * @throws IllegalStateException when the user is locked.
278      */
getShortcuts(final @NonNull Context context, @ShortcutMatchFlags int matchFlags)279     public static @NonNull List<ShortcutInfoCompat> getShortcuts(final @NonNull Context context,
280             @ShortcutMatchFlags int matchFlags) {
281         if (Build.VERSION.SDK_INT >= 30) {
282             final List<ShortcutInfo> shortcuts =
283                     context.getSystemService(ShortcutManager.class).getShortcuts(matchFlags);
284             return ShortcutInfoCompat.fromShortcuts(context, shortcuts);
285         } else if (Build.VERSION.SDK_INT >= 25) {
286             final ShortcutManager manager = context.getSystemService(ShortcutManager.class);
287             final List<ShortcutInfo> shortcuts = new ArrayList<>();
288             if ((matchFlags & FLAG_MATCH_MANIFEST) != 0) {
289                 shortcuts.addAll(manager.getManifestShortcuts());
290             }
291             if ((matchFlags & FLAG_MATCH_DYNAMIC) != 0) {
292                 shortcuts.addAll(manager.getDynamicShortcuts());
293             }
294             if ((matchFlags & FLAG_MATCH_PINNED) != 0) {
295                 shortcuts.addAll(manager.getPinnedShortcuts());
296             }
297             return ShortcutInfoCompat.fromShortcuts(context, shortcuts);
298         }
299         if ((matchFlags & FLAG_MATCH_DYNAMIC) != 0) {
300             try {
301                 return getShortcutInfoSaverInstance(context).getShortcuts();
302             } catch (Exception e) {
303                 // Ignore
304             }
305         }
306         return Collections.emptyList();
307     }
308 
309     /**
310      * Publish the list of dynamic shortcuts. If there are already dynamic or pinned shortcuts with
311      * the same IDs, each mutable shortcut is updated.
312      * <p>On API <= 31 Any shortcuts that are marked as excluded from launcher will not be passed
313      * to the {@link ShortcutManager}, but they might still be available to assistant and other
314      * surfaces through alternative means.
315      *
316      * <p>This API will be rate-limited.
317      *
318      * @return {@code true} if the call has succeeded. {@code false} if the call fails or is
319      * rate-limited.
320      *
321      * @throws IllegalArgumentException if {@link #getMaxShortcutCountPerActivity(Context)} is
322      * exceeded, or when trying to update immutable shortcuts.
323      */
addDynamicShortcuts(@onNull Context context, @NonNull List<ShortcutInfoCompat> shortcutInfoList)324     public static boolean addDynamicShortcuts(@NonNull Context context,
325             @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
326         final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
327                 shortcutInfoList, ShortcutInfoCompat.SURFACE_LAUNCHER);
328         if (Build.VERSION.SDK_INT <= 29) {
329             convertUriIconsToBitmapIcons(context, clone);
330         }
331         if (Build.VERSION.SDK_INT >= 25) {
332             ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
333             for (ShortcutInfoCompat item : clone) {
334                 shortcuts.add(item.toShortcutInfo());
335             }
336             if (!context.getSystemService(ShortcutManager.class).addDynamicShortcuts(shortcuts)) {
337                 return false;
338             }
339         }
340 
341         getShortcutInfoSaverInstance(context).addShortcuts(clone);
342         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
343             listener.onShortcutAdded(shortcutInfoList);
344         }
345         return true;
346     }
347 
348     /**
349      * @return The maximum number of static and dynamic shortcuts that each launcher icon
350      * can have at a time.
351      */
getMaxShortcutCountPerActivity(@onNull Context context)352     public static int getMaxShortcutCountPerActivity(@NonNull Context context) {
353         Preconditions.checkNotNull(context);
354         if (Build.VERSION.SDK_INT >= 25) {
355             return context.getSystemService(ShortcutManager.class).getMaxShortcutCountPerActivity();
356         }
357 
358         return 5;
359     }
360 
361     /**
362      * Return {@code true} when rate-limiting is active for the caller app.
363      *
364      * <p>For details, see <a href="/guide/topics/ui/shortcuts/managing-shortcuts#rate-limiting">
365      * Rate limiting</a>.
366      *
367      * @throws IllegalStateException when the user is locked.
368      */
isRateLimitingActive(final @NonNull Context context)369     public static boolean isRateLimitingActive(final @NonNull Context context) {
370         Preconditions.checkNotNull(context);
371         if (Build.VERSION.SDK_INT >= 25) {
372             return context.getSystemService(ShortcutManager.class).isRateLimitingActive();
373         }
374 
375         return getShortcuts(context, FLAG_MATCH_MANIFEST | FLAG_MATCH_DYNAMIC).size()
376                 == getMaxShortcutCountPerActivity(context);
377     }
378 
379     /**
380      * Return the max width for icons, in pixels.
381      *
382      * <p> Note that this method returns max width of icon's visible part. Hence, it does not take
383      * into account the inset introduced by {@link android.graphics.drawable.AdaptiveIconDrawable}.
384      * To calculate bitmap image to function as
385      * {@link android.graphics.drawable.AdaptiveIconDrawable}, multiply
386      * 1 + 2 * {@link android.graphics.drawable.AdaptiveIconDrawable#getExtraInsetFraction()} to
387      * the returned size.
388      */
getIconMaxWidth(final @NonNull Context context)389     public static int getIconMaxWidth(final @NonNull Context context) {
390         Preconditions.checkNotNull(context);
391         if (Build.VERSION.SDK_INT >= 25) {
392             return context.getSystemService(ShortcutManager.class).getIconMaxWidth();
393         }
394         return getIconDimensionInternal(context, true);
395     }
396 
397     /**
398      * Return the max height for icons, in pixels.
399      */
getIconMaxHeight(final @NonNull Context context)400     public static int getIconMaxHeight(final @NonNull Context context) {
401         Preconditions.checkNotNull(context);
402         if (Build.VERSION.SDK_INT >= 25) {
403             return context.getSystemService(ShortcutManager.class).getIconMaxHeight();
404         }
405         return getIconDimensionInternal(context, false);
406     }
407 
408     /**
409      * Apps that publish shortcuts should call this method whenever the user
410      * selects the shortcut containing the given ID or when the user completes
411      * an action in the app that is equivalent to selecting the shortcut.
412      * For more details, read about
413      * <a href="/guide/topics/ui/shortcuts/managing-shortcuts.html#track-usage">
414      * tracking shortcut usage</a>.
415      *
416      * <p>The information is accessible via {@link android.app.usage.UsageStatsManager#queryEvents}
417      * Typically, launcher apps use this information to build a prediction model
418      * so that they can promote the shortcuts that are likely to be used at the moment.
419      *
420      * @throws IllegalStateException when the user is locked.
421      *
422      * <p>This method is not supported on devices running SDK < 25 since the platform class will
423      * not be available.
424      */
reportShortcutUsed(final @NonNull Context context, final @NonNull String shortcutId)425     public static void reportShortcutUsed(final @NonNull Context context,
426             final @NonNull String shortcutId) {
427         Preconditions.checkNotNull(context);
428         Preconditions.checkNotNull(shortcutId);
429         if (Build.VERSION.SDK_INT >= 25) {
430             context.getSystemService(ShortcutManager.class).reportShortcutUsed(shortcutId);
431         }
432 
433         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
434             listener.onShortcutUsageReported(Collections.singletonList(shortcutId));
435         }
436     }
437 
438     /**
439      * Publish the list of shortcuts.  All existing dynamic shortcuts from the caller app
440      * will be replaced.  If there are already pinned shortcuts with the same IDs,
441      * the mutable pinned shortcuts are updated.
442      * <p>On API <= 31 Any shortcuts that are marked as excluded from launcher will not be passed
443      * to the {@link ShortcutManager}, but they might still be available to assistant and other
444      * surfaces through alternative means.
445      *
446      * <p>This API will be rate-limited.
447      *
448      * Compatibility behavior:
449      * <ul>
450      *      <li>API 25 and above, this method matches platform behavior.
451      *      <li>API 24 and earlier, this method is equivalent of calling
452      *      {@link #removeAllDynamicShortcuts} and {@link #addDynamicShortcuts} consecutively.
453      * </ul>
454      *
455      * @return {@code true} if the call has succeeded. {@code false} if the call is rate-limited.
456      *
457      * @throws IllegalArgumentException if {@link #getMaxShortcutCountPerActivity} is exceeded,
458      * or when trying to update immutable shortcuts.
459      *
460      * @throws IllegalStateException when the user is locked.
461      */
setDynamicShortcuts(final @NonNull Context context, final @NonNull List<ShortcutInfoCompat> shortcutInfoList)462     public static boolean setDynamicShortcuts(final @NonNull Context context,
463             final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
464         Preconditions.checkNotNull(context);
465         Preconditions.checkNotNull(shortcutInfoList);
466         final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
467                 shortcutInfoList, ShortcutInfoCompat.SURFACE_LAUNCHER);
468         if (Build.VERSION.SDK_INT >= 25) {
469             List<ShortcutInfo> shortcuts = new ArrayList<>(clone.size());
470             for (ShortcutInfoCompat compat : clone) {
471                 shortcuts.add(compat.toShortcutInfo());
472             }
473             if (!context.getSystemService(ShortcutManager.class).setDynamicShortcuts(shortcuts)) {
474                 return false;
475             }
476         }
477         getShortcutInfoSaverInstance(context).removeAllShortcuts();
478         getShortcutInfoSaverInstance(context).addShortcuts(clone);
479 
480         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
481             listener.onAllShortcutsRemoved();
482             listener.onShortcutAdded(shortcutInfoList);
483         }
484         return true;
485     }
486 
487     /**
488      * Return all dynamic shortcuts from the caller app.
489      *
490      * <p>This API is intended to be used for examining what shortcuts are currently published.
491      * Re-publishing returned {@link ShortcutInfo}s via APIs such as
492      * {@link #addDynamicShortcuts(Context, List)} may cause loss of information such as icons.
493      */
getDynamicShortcuts(@onNull Context context)494     public static @NonNull List<ShortcutInfoCompat> getDynamicShortcuts(@NonNull Context context) {
495         if (Build.VERSION.SDK_INT >= 25) {
496             List<ShortcutInfo> shortcuts = context.getSystemService(
497                     ShortcutManager.class).getDynamicShortcuts();
498             List<ShortcutInfoCompat> compats = new ArrayList<>(shortcuts.size());
499             for (ShortcutInfo item : shortcuts) {
500                 compats.add(new ShortcutInfoCompat.Builder(context, item).build());
501             }
502             return compats;
503         }
504 
505         try {
506             return getShortcutInfoSaverInstance(context).getShortcuts();
507         } catch (Exception e) {
508             /* Do nothing */
509         }
510 
511         return new ArrayList<>();
512     }
513 
514     /**
515      * Update all existing shortcuts with the same IDs. Target shortcuts may be pinned and/or
516      * dynamic, but they must not be immutable.
517      * <p>On API <= 31 Any shortcuts that are marked as excluded from launcher will not be passed
518      * to the {@link ShortcutManager}, but they might still be available to assistant and other
519      * surfaces through alternative means.
520      *
521      * <p>This API will be rate-limited.
522      *
523      * @return {@code true} if the call has succeeded. {@code false} if the call fails or is
524      * rate-limited.
525      *
526      * @throws IllegalArgumentException If trying to update immutable shortcuts.
527      */
updateShortcuts(@onNull Context context, @NonNull List<ShortcutInfoCompat> shortcutInfoList)528     public static boolean updateShortcuts(@NonNull Context context,
529             @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
530         final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
531                 shortcutInfoList, ShortcutInfoCompat.SURFACE_LAUNCHER);
532         if (Build.VERSION.SDK_INT <= 29) {
533             convertUriIconsToBitmapIcons(context, clone);
534         }
535         if (Build.VERSION.SDK_INT >= 25) {
536             ArrayList<ShortcutInfo> shortcuts = new ArrayList<>();
537             for (ShortcutInfoCompat item : clone) {
538                 shortcuts.add(item.toShortcutInfo());
539             }
540             if (!context.getSystemService(ShortcutManager.class).updateShortcuts(shortcuts)) {
541                 return false;
542             }
543         }
544 
545         getShortcutInfoSaverInstance(context).addShortcuts(clone);
546         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
547             listener.onShortcutUpdated(shortcutInfoList);
548         }
549         return true;
550     }
551 
552     @VisibleForTesting
convertUriIconToBitmapIcon(final @NonNull Context context, final @NonNull ShortcutInfoCompat info)553     static boolean convertUriIconToBitmapIcon(final @NonNull Context context,
554             final @NonNull ShortcutInfoCompat info) {
555         if (info.mIcon == null) {
556             return false;
557         }
558         final int type = info.mIcon.mType;
559         if (type != TYPE_URI_ADAPTIVE_BITMAP && type != TYPE_URI) {
560             return true;
561         }
562         InputStream is = info.mIcon.getUriInputStream(context);
563         if (is == null) {
564             return false;
565         }
566         final Bitmap bitmap = BitmapFactory.decodeStream(is);
567         if (bitmap == null) {
568             return false;
569         }
570         info.mIcon = (type == TYPE_URI_ADAPTIVE_BITMAP)
571                 ? IconCompat.createWithAdaptiveBitmap(bitmap)
572                 : IconCompat.createWithBitmap(bitmap);
573         return true;
574     }
575 
576     @VisibleForTesting
convertUriIconsToBitmapIcons(final @NonNull Context context, final @NonNull List<ShortcutInfoCompat> shortcutInfoList)577     static void convertUriIconsToBitmapIcons(final @NonNull Context context,
578             final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
579         final List<ShortcutInfoCompat> shortcuts = new ArrayList<>(shortcutInfoList);
580         for (ShortcutInfoCompat info : shortcuts) {
581             if (!convertUriIconToBitmapIcon(context, info)) {
582                 shortcutInfoList.remove(info);
583             }
584         }
585     }
586 
587     /**
588      * Disable pinned shortcuts, showing the user a custom error message when they try to select
589      * the disabled shortcuts.
590      * For more details, read
591      * <a href="/guide/topics/ui/shortcuts/managing-shortcuts.html#disable-shortcuts">
592      * Disable shortcuts</a>.
593      *
594      * Compatibility behavior:
595      * <ul>
596      *      <li>API 25 and above, this method matches platform behavior.
597      *      <li>API 24 and earlier, this method behaves the same as {@link #removeDynamicShortcuts}
598      * </ul>
599      *
600      * @throws IllegalArgumentException If trying to disable immutable shortcuts.
601      *
602      * @throws IllegalStateException when the user is locked.
603      */
disableShortcuts(final @NonNull Context context, final @NonNull List<String> shortcutIds, final @Nullable CharSequence disabledMessage)604     public static void disableShortcuts(final @NonNull Context context,
605             final @NonNull List<String> shortcutIds, final @Nullable CharSequence disabledMessage) {
606         if (Build.VERSION.SDK_INT >= 25) {
607             context.getSystemService(ShortcutManager.class)
608                     .disableShortcuts(shortcutIds, disabledMessage);
609         }
610 
611         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
612         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
613             listener.onShortcutRemoved(shortcutIds);
614         }
615     }
616 
617     /**
618      * Re-enable pinned shortcuts that were previously disabled.  If the target shortcuts
619      * are already enabled, this method does nothing.
620      * <p>In API 31 and below any shortcuts that are marked as excluded from launcher will be
621      * ignored.
622      *
623      * Compatibility behavior:
624      * <ul>
625      *      <li>API 25 and above, this method matches platform behavior.
626      *      <li>API 24 and earlier, this method behaves the same as {@link #addDynamicShortcuts}
627      * </ul>
628      *
629      * @throws IllegalArgumentException If trying to enable immutable shortcuts.
630      *
631      * @throws IllegalStateException when the user is locked.
632      */
enableShortcuts(final @NonNull Context context, final @NonNull List<ShortcutInfoCompat> shortcutInfoList)633     public static void enableShortcuts(final @NonNull Context context,
634             final @NonNull List<ShortcutInfoCompat> shortcutInfoList) {
635         final List<ShortcutInfoCompat> clone = removeShortcutsExcludedFromSurface(
636                 shortcutInfoList, ShortcutInfoCompat.SURFACE_LAUNCHER);
637         if (Build.VERSION.SDK_INT >= 25) {
638             final ArrayList<String> shortcutIds = new ArrayList<>(shortcutInfoList.size());
639             for (ShortcutInfoCompat shortcut : clone) {
640                 shortcutIds.add(shortcut.mId);
641             }
642             context.getSystemService(ShortcutManager.class).enableShortcuts(shortcutIds);
643         }
644 
645         getShortcutInfoSaverInstance(context).addShortcuts(clone);
646         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
647             listener.onShortcutAdded(shortcutInfoList);
648         }
649     }
650 
651     /**
652      * Delete dynamic shortcuts by ID. Note that if a shortcut is set as long-lived, it may still
653      * be available in the system as a cached shortcut even after being removed from the list of
654      * dynamic shortcuts.
655      *
656      * @see #removeLongLivedShortcuts
657      */
removeDynamicShortcuts(@onNull Context context, @NonNull List<String> shortcutIds)658     public static void removeDynamicShortcuts(@NonNull Context context,
659             @NonNull List<String> shortcutIds) {
660         if (Build.VERSION.SDK_INT >= 25) {
661             context.getSystemService(ShortcutManager.class).removeDynamicShortcuts(shortcutIds);
662         }
663 
664         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
665         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
666             listener.onShortcutRemoved(shortcutIds);
667         }
668     }
669 
670     /**
671      * Delete all dynamic shortcuts from the caller app. Note that if a shortcut is set as
672      * long-lived, it may still be available in the system as a cached shortcut even after being
673      * removed from the list of dynamic shortcuts.
674      *
675      * @see #removeLongLivedShortcuts
676      */
removeAllDynamicShortcuts(@onNull Context context)677     public static void removeAllDynamicShortcuts(@NonNull Context context) {
678         if (Build.VERSION.SDK_INT >= 25) {
679             context.getSystemService(ShortcutManager.class).removeAllDynamicShortcuts();
680         }
681 
682         getShortcutInfoSaverInstance(context).removeAllShortcuts();
683         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
684             listener.onAllShortcutsRemoved();
685         }
686     }
687 
688     /**
689      * Delete long lived shortcuts by ID.
690      *
691      * Compatibility behavior:
692      * <ul>
693      *      <li>API 30 and above, this method matches platform behavior.
694      *      <li>API 29 and earlier, this method behaves the same as {@link #removeDynamicShortcuts}
695      * </ul>
696      *
697      * @throws IllegalStateException when the user is locked.
698      */
removeLongLivedShortcuts(final @NonNull Context context, final @NonNull List<String> shortcutIds)699     public static void removeLongLivedShortcuts(final @NonNull Context context,
700             final @NonNull List<String> shortcutIds) {
701         if (Build.VERSION.SDK_INT < 30) {
702             removeDynamicShortcuts(context, shortcutIds);
703             return;
704         }
705 
706         context.getSystemService(ShortcutManager.class).removeLongLivedShortcuts(shortcutIds);
707         getShortcutInfoSaverInstance(context).removeShortcuts(shortcutIds);
708         for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
709             listener.onShortcutRemoved(shortcutIds);
710         }
711     }
712 
713     /**
714      * Publish a single dynamic shortcut. If there are already dynamic or pinned shortcuts with the
715      * same ID, each mutable shortcut is updated.
716      *
717      * <p>This method is useful when posting notifications which are tagged with shortcut IDs; In
718      * order to make sure shortcuts exist and are up-to-date, without the need to explicitly handle
719      * the shortcut count limit.
720      * @see androidx.core.app.NotificationManagerCompat#notify(int, android.app.Notification)
721      * @see androidx.core.app.NotificationCompat.Builder#setShortcutId(String)
722      *
723      * <p>If {@link #getMaxShortcutCountPerActivity} is already reached, an existing shortcut with
724      * the lowest rank will be removed to add space for the new shortcut.
725      *
726      * <p>If the rank of the shortcut is not explicitly set, it will be set to zero, and shortcut
727      * will be added to the top of the list.
728      *
729      * Compatibility behavior:
730      * <ul>
731      *      <li>API 30 and above, this method matches platform behavior.
732      *      <li>API 25 to 29, this api is simulated by
733      *      {@link ShortcutManager#addDynamicShortcuts(List)} and
734      *      {@link ShortcutManager#removeDynamicShortcuts(List)} and thus will be rate-limited.
735      *      <li>API 24 and earlier, this method uses internal implementation and matches platform
736      *      behavior.
737      * </ul>
738      *
739      * @return {@code true} if the call has succeeded. {@code false} if the call fails or is
740      * rate-limited.
741      *
742      * @throws IllegalArgumentException if trying to update an immutable shortcut.
743      *
744      * @throws IllegalStateException when the user is locked.
745      */
pushDynamicShortcut(final @NonNull Context context, final @NonNull ShortcutInfoCompat shortcut)746     public static boolean pushDynamicShortcut(final @NonNull Context context,
747             final @NonNull ShortcutInfoCompat shortcut) {
748         Preconditions.checkNotNull(context);
749         Preconditions.checkNotNull(shortcut);
750 
751         if (Build.VERSION.SDK_INT <= 32
752                 && shortcut.isExcludedFromSurfaces(ShortcutInfoCompat.SURFACE_LAUNCHER)) {
753             for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
754                 listener.onShortcutAdded(Collections.singletonList(shortcut));
755             }
756             return true;
757         }
758         int maxShortcutCount = getMaxShortcutCountPerActivity(context);
759         if (maxShortcutCount == 0) {
760             return false;
761         }
762         if (Build.VERSION.SDK_INT <= 29) {
763             convertUriIconToBitmapIcon(context, shortcut);
764         }
765         if (Build.VERSION.SDK_INT >= 30) {
766             context.getSystemService(ShortcutManager.class).pushDynamicShortcut(
767                     shortcut.toShortcutInfo());
768         } else if (Build.VERSION.SDK_INT >= 25) {
769             final ShortcutManager sm = context.getSystemService(ShortcutManager.class);
770             if (sm.isRateLimitingActive()) {
771                 return false;
772             }
773             final List<ShortcutInfo> shortcuts = sm.getDynamicShortcuts();
774             if (shortcuts.size() >= maxShortcutCount) {
775                 sm.removeDynamicShortcuts(Arrays.asList(
776                         Api25Impl.getShortcutInfoWithLowestRank(shortcuts)));
777             }
778             sm.addDynamicShortcuts(Arrays.asList(shortcut.toShortcutInfo()));
779         }
780         final ShortcutInfoCompatSaver<?> saver = getShortcutInfoSaverInstance(context);
781         try {
782             final List<ShortcutInfoCompat> oldShortcuts = saver.getShortcuts();
783             if (oldShortcuts.size() >= maxShortcutCount) {
784                 saver.removeShortcuts(Arrays.asList(
785                         getShortcutInfoCompatWithLowestRank(oldShortcuts)));
786             }
787             saver.addShortcuts(Arrays.asList(shortcut));
788             return true;
789         } catch (Exception e) {
790             // Ignore
791         } finally {
792             for (ShortcutInfoChangeListener listener : getShortcutInfoListeners(context)) {
793                 listener.onShortcutAdded(Collections.singletonList(shortcut));
794             }
795             reportShortcutUsed(context, shortcut.getId());
796         }
797         return false;
798     }
799 
getShortcutInfoCompatWithLowestRank( final @NonNull List<ShortcutInfoCompat> shortcuts)800     private static String getShortcutInfoCompatWithLowestRank(
801             final @NonNull List<ShortcutInfoCompat> shortcuts) {
802         int rank = -1;
803         String target = null;
804         for (ShortcutInfoCompat s : shortcuts) {
805             if (s.getRank() > rank) {
806                 target = s.getId();
807                 rank = s.getRank();
808             }
809         }
810         return target;
811     }
812 
813     @VisibleForTesting
setShortcutInfoCompatSaver(final ShortcutInfoCompatSaver<Void> saver)814     static void setShortcutInfoCompatSaver(final ShortcutInfoCompatSaver<Void> saver) {
815         sShortcutInfoCompatSaver = saver;
816     }
817 
818     @VisibleForTesting
setShortcutInfoChangeListeners(final List<ShortcutInfoChangeListener> listeners)819     static void setShortcutInfoChangeListeners(final List<ShortcutInfoChangeListener> listeners) {
820         sShortcutInfoChangeListeners = listeners;
821     }
822 
823     @VisibleForTesting
getShortcutInfoChangeListeners()824     static List<ShortcutInfoChangeListener> getShortcutInfoChangeListeners() {
825         return sShortcutInfoChangeListeners;
826     }
827 
getIconDimensionInternal(final @NonNull Context context, final boolean isHorizontal)828     private static int getIconDimensionInternal(final @NonNull Context context,
829             final boolean isHorizontal) {
830         final ActivityManager am = (ActivityManager)
831                 context.getSystemService(Context.ACTIVITY_SERVICE);
832         final boolean isLowRamDevice = am == null || am.isLowRamDevice();
833         final int iconDimensionDp = Math.max(1, isLowRamDevice
834                 ? DEFAULT_MAX_ICON_DIMENSION_LOWRAM_DP : DEFAULT_MAX_ICON_DIMENSION_DP);
835         final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
836         float density = (isHorizontal ? displayMetrics.xdpi : displayMetrics.ydpi)
837                 / DisplayMetrics.DENSITY_MEDIUM;
838         return (int) (iconDimensionDp * density);
839     }
840 
getShortcutInfoSaverInstance(Context context)841     private static ShortcutInfoCompatSaver<?> getShortcutInfoSaverInstance(Context context) {
842         if (sShortcutInfoCompatSaver == null) {
843             if (Build.VERSION.SDK_INT >= 23) {
844                 try {
845                     ClassLoader loader = ShortcutManagerCompat.class.getClassLoader();
846                     Class<?> saver = Class.forName(
847                             "androidx.sharetarget.ShortcutInfoCompatSaverImpl", false, loader);
848                     Method getInstanceMethod = saver.getMethod("getInstance", Context.class);
849                     sShortcutInfoCompatSaver = (ShortcutInfoCompatSaver) getInstanceMethod.invoke(
850                             null, context);
851                 } catch (Exception e) { /* Do nothing */ }
852             }
853 
854             if (sShortcutInfoCompatSaver == null) {
855                 // Implementation not available. Instantiate to the default no-op impl.
856                 sShortcutInfoCompatSaver = new ShortcutInfoCompatSaver.NoopImpl();
857             }
858         }
859         return sShortcutInfoCompatSaver;
860     }
861 
862     @SuppressWarnings("deprecation")
getShortcutInfoListeners(Context context)863     private static List<ShortcutInfoChangeListener> getShortcutInfoListeners(Context context) {
864         if (sShortcutInfoChangeListeners == null) {
865             List<ShortcutInfoChangeListener> result = new ArrayList<>();
866             if (Build.VERSION.SDK_INT >= 21) {
867                 PackageManager packageManager = context.getPackageManager();
868                 Intent activityIntent = new Intent(SHORTCUT_LISTENER_INTENT_FILTER_ACTION);
869                 activityIntent.setPackage(context.getPackageName());
870 
871                 List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
872                         activityIntent, PackageManager.GET_META_DATA);
873 
874                 for (ResolveInfo resolveInfo : resolveInfos) {
875                     ActivityInfo activityInfo = resolveInfo.activityInfo;
876                     if (activityInfo == null) {
877                         continue;
878                     }
879                     Bundle metaData = activityInfo.metaData;
880                     if (metaData == null) {
881                         continue;
882                     }
883                     String shortcutListenerImplName =
884                             metaData.getString(SHORTCUT_LISTENER_META_DATA_KEY);
885                     if (shortcutListenerImplName == null) {
886                         continue;
887                     }
888                     try {
889                         ClassLoader loader = ShortcutManagerCompat.class.getClassLoader();
890                         Class<?> listener = Class.forName(shortcutListenerImplName, false, loader);
891                         Method getInstanceMethod = listener.getMethod("getInstance", Context.class);
892                         result.add((ShortcutInfoChangeListener)
893                                 getInstanceMethod.invoke(null, context));
894                     } catch (Exception e) { /* Do nothing */ }
895                 }
896             }
897 
898             // Make sure the listeners are not already added while the loop is running.
899             if (sShortcutInfoChangeListeners == null) {
900                 sShortcutInfoChangeListeners = result;
901             }
902         }
903         return sShortcutInfoChangeListeners;
904     }
905 
removeShortcutsExcludedFromSurface( final @NonNull List<ShortcutInfoCompat> shortcuts, final int surfaces)906     private static @NonNull List<ShortcutInfoCompat> removeShortcutsExcludedFromSurface(
907             final @NonNull List<ShortcutInfoCompat> shortcuts, final int surfaces) {
908         Objects.requireNonNull(shortcuts);
909         if (Build.VERSION.SDK_INT > 32) return shortcuts;
910         final List<ShortcutInfoCompat> clone = new ArrayList<>(shortcuts);
911         for (ShortcutInfoCompat si: shortcuts) {
912             if (si.isExcludedFromSurfaces(surfaces)) {
913                 clone.remove(si);
914             }
915         }
916         return clone;
917     }
918 
919     @RequiresApi(25)
920     private static class Api25Impl {
getShortcutInfoWithLowestRank(final @NonNull List<ShortcutInfo> shortcuts)921         static String getShortcutInfoWithLowestRank(final @NonNull List<ShortcutInfo> shortcuts) {
922             int rank = -1;
923             String target = null;
924             for (ShortcutInfo s : shortcuts) {
925                 if (s.getRank() > rank) {
926                     target = s.getId();
927                     rank = s.getRank();
928                 }
929             }
930             return target;
931         }
932     }
933 }
934