1 package org.robolectric.shadows;
2 
3 import static android.content.pm.ShortcutManager.FLAG_MATCH_CACHED;
4 import static android.content.pm.ShortcutManager.FLAG_MATCH_DYNAMIC;
5 import static android.content.pm.ShortcutManager.FLAG_MATCH_MANIFEST;
6 import static android.content.pm.ShortcutManager.FLAG_MATCH_PINNED;
7 import static android.os.Build.VERSION_CODES.R;
8 import static java.util.stream.Collectors.toCollection;
9 
10 import android.content.Intent;
11 import android.content.IntentSender;
12 import android.content.IntentSender.SendIntentException;
13 import android.content.pm.ShortcutInfo;
14 import android.content.pm.ShortcutManager;
15 import android.os.Build;
16 import android.os.Build.VERSION_CODES;
17 import com.google.common.collect.ImmutableList;
18 import com.google.common.collect.Lists;
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.HashMap;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import org.robolectric.RuntimeEnvironment;
27 import org.robolectric.annotation.Implementation;
28 import org.robolectric.annotation.Implements;
29 import org.robolectric.annotation.Resetter;
30 
31 /** */
32 @Implements(value = ShortcutManager.class, minSdk = Build.VERSION_CODES.N_MR1)
33 public class ShadowShortcutManager {
34 
35   private static final int MAX_ICON_DIMENSION = 128;
36 
37   private static final Map<String, ShortcutInfo> dynamicShortcuts = new HashMap<>();
38   private static final Map<String, ShortcutInfo> activePinnedShortcuts = new HashMap<>();
39   private static final Map<String, ShortcutInfo> disabledPinnedShortcuts = new HashMap<>();
40 
41   private static List<ShortcutInfo> manifestShortcuts = ImmutableList.of();
42 
43   private static boolean isRequestPinShortcutSupported = true;
44   private static int maxShortcutCountPerActivity = 16;
45   private static int maxIconHeight = MAX_ICON_DIMENSION;
46   private static int maxIconWidth = MAX_ICON_DIMENSION;
47 
48   @Resetter
reset()49   public static void reset() {
50     dynamicShortcuts.clear();
51     activePinnedShortcuts.clear();
52     disabledPinnedShortcuts.clear();
53     manifestShortcuts = ImmutableList.of();
54     isRequestPinShortcutSupported = true;
55     maxShortcutCountPerActivity = 16;
56     maxIconHeight = MAX_ICON_DIMENSION;
57     maxIconWidth = MAX_ICON_DIMENSION;
58   }
59 
60   @Implementation
addDynamicShortcuts(List<ShortcutInfo> shortcutInfoList)61   protected boolean addDynamicShortcuts(List<ShortcutInfo> shortcutInfoList) {
62     for (ShortcutInfo shortcutInfo : shortcutInfoList) {
63       shortcutInfo.addFlags(ShortcutInfo.FLAG_DYNAMIC);
64       if (activePinnedShortcuts.containsKey(shortcutInfo.getId())) {
65         ShortcutInfo previousShortcut = activePinnedShortcuts.get(shortcutInfo.getId());
66         if (!previousShortcut.isImmutable()) {
67           activePinnedShortcuts.put(shortcutInfo.getId(), shortcutInfo);
68         }
69       } else if (disabledPinnedShortcuts.containsKey(shortcutInfo.getId())) {
70         ShortcutInfo previousShortcut = disabledPinnedShortcuts.get(shortcutInfo.getId());
71         if (!previousShortcut.isImmutable()) {
72           disabledPinnedShortcuts.put(shortcutInfo.getId(), shortcutInfo);
73         }
74       } else if (dynamicShortcuts.containsKey(shortcutInfo.getId())) {
75         ShortcutInfo previousShortcut = dynamicShortcuts.get(shortcutInfo.getId());
76         if (!previousShortcut.isImmutable()) {
77           dynamicShortcuts.put(shortcutInfo.getId(), shortcutInfo);
78         }
79       } else {
80         dynamicShortcuts.put(shortcutInfo.getId(), shortcutInfo);
81       }
82     }
83     return true;
84   }
85 
86   @Implementation(minSdk = Build.VERSION_CODES.O)
createShortcutResultIntent(ShortcutInfo shortcut)87   protected Intent createShortcutResultIntent(ShortcutInfo shortcut) {
88     if (disabledPinnedShortcuts.containsKey(shortcut.getId())) {
89       throw new IllegalArgumentException();
90     }
91     return new Intent();
92   }
93 
94   @Implementation
disableShortcuts(List<String> shortcutIds)95   protected void disableShortcuts(List<String> shortcutIds) {
96     disableShortcuts(shortcutIds, "Shortcut is disabled.");
97   }
98 
99   @Implementation
disableShortcuts(List<String> shortcutIds, CharSequence unused)100   protected void disableShortcuts(List<String> shortcutIds, CharSequence unused) {
101     for (String shortcutId : shortcutIds) {
102       ShortcutInfo shortcut = activePinnedShortcuts.remove(shortcutId);
103       if (shortcut != null) {
104         disabledPinnedShortcuts.put(shortcutId, shortcut);
105       }
106     }
107   }
108 
109   @Implementation
enableShortcuts(List<String> shortcutIds)110   protected void enableShortcuts(List<String> shortcutIds) {
111     for (String shortcutId : shortcutIds) {
112       ShortcutInfo shortcut = disabledPinnedShortcuts.remove(shortcutId);
113       if (shortcut != null) {
114         activePinnedShortcuts.put(shortcutId, shortcut);
115       }
116     }
117   }
118 
119   @Implementation
getDynamicShortcuts()120   protected List<ShortcutInfo> getDynamicShortcuts() {
121     return ImmutableList.copyOf(dynamicShortcuts.values());
122   }
123 
124   @Implementation
getIconMaxHeight()125   protected int getIconMaxHeight() {
126     return maxIconHeight;
127   }
128 
129   @Implementation
getIconMaxWidth()130   protected int getIconMaxWidth() {
131     return maxIconWidth;
132   }
133 
134   /** Sets the value returned by {@link #getIconMaxHeight()}. */
setIconMaxHeight(int height)135   public void setIconMaxHeight(int height) {
136     maxIconHeight = height;
137   }
138 
139   /** Sets the value returned by {@link #getIconMaxWidth()}. */
setIconMaxWidth(int width)140   public void setIconMaxWidth(int width) {
141     maxIconWidth = width;
142   }
143 
144   @Implementation
getManifestShortcuts()145   protected List<ShortcutInfo> getManifestShortcuts() {
146     return manifestShortcuts;
147   }
148 
149   /** Sets the value returned by {@link #getManifestShortcuts()}. */
setManifestShortcuts(List<ShortcutInfo> manifestShortcuts)150   public void setManifestShortcuts(List<ShortcutInfo> manifestShortcuts) {
151     for (ShortcutInfo shortcutInfo : manifestShortcuts) {
152       shortcutInfo.addFlags(ShortcutInfo.FLAG_MANIFEST);
153     }
154     this.manifestShortcuts = manifestShortcuts;
155   }
156 
157   @Implementation
getMaxShortcutCountPerActivity()158   protected int getMaxShortcutCountPerActivity() {
159     return maxShortcutCountPerActivity;
160   }
161 
162   /** Sets the value returned by {@link #getMaxShortcutCountPerActivity()} . */
setMaxShortcutCountPerActivity(int value)163   public void setMaxShortcutCountPerActivity(int value) {
164     maxShortcutCountPerActivity = value;
165   }
166 
167   @Implementation
getPinnedShortcuts()168   protected List<ShortcutInfo> getPinnedShortcuts() {
169     ImmutableList.Builder<ShortcutInfo> pinnedShortcuts = ImmutableList.builder();
170     pinnedShortcuts.addAll(activePinnedShortcuts.values());
171     pinnedShortcuts.addAll(disabledPinnedShortcuts.values());
172     return pinnedShortcuts.build();
173   }
174 
175   @Implementation
isRateLimitingActive()176   protected boolean isRateLimitingActive() {
177     return false;
178   }
179 
180   @Implementation(minSdk = Build.VERSION_CODES.O)
isRequestPinShortcutSupported()181   protected boolean isRequestPinShortcutSupported() {
182     return isRequestPinShortcutSupported;
183   }
184 
setIsRequestPinShortcutSupported(boolean isRequestPinShortcutSupported)185   public void setIsRequestPinShortcutSupported(boolean isRequestPinShortcutSupported) {
186     this.isRequestPinShortcutSupported = isRequestPinShortcutSupported;
187   }
188 
189   @Implementation
removeAllDynamicShortcuts()190   protected void removeAllDynamicShortcuts() {
191     dynamicShortcuts.clear();
192   }
193 
194   @Implementation
removeDynamicShortcuts(List<String> shortcutIds)195   protected void removeDynamicShortcuts(List<String> shortcutIds) {
196     for (String shortcutId : shortcutIds) {
197       dynamicShortcuts.remove(shortcutId);
198     }
199   }
200 
201   @Implementation
reportShortcutUsed(String shortcutId)202   protected void reportShortcutUsed(String shortcutId) {}
203 
204   @Implementation(minSdk = Build.VERSION_CODES.O)
requestPinShortcut(ShortcutInfo shortcut, IntentSender resultIntent)205   protected boolean requestPinShortcut(ShortcutInfo shortcut, IntentSender resultIntent) {
206     shortcut.addFlags(ShortcutInfo.FLAG_PINNED);
207     if (disabledPinnedShortcuts.containsKey(shortcut.getId())) {
208       throw new IllegalArgumentException(
209           "Shortcut with ID [" + shortcut.getId() + "] already exists and is disabled.");
210     }
211     if (dynamicShortcuts.containsKey(shortcut.getId())) {
212       activePinnedShortcuts.put(shortcut.getId(), dynamicShortcuts.remove(shortcut.getId()));
213     } else {
214       activePinnedShortcuts.put(shortcut.getId(), shortcut);
215     }
216     if (resultIntent != null) {
217       try {
218         resultIntent.sendIntent(RuntimeEnvironment.getApplication(), 0, null, null, null);
219       } catch (SendIntentException e) {
220         throw new IllegalStateException();
221       }
222     }
223     return true;
224   }
225 
226   @Implementation
setDynamicShortcuts(List<ShortcutInfo> shortcutInfoList)227   protected boolean setDynamicShortcuts(List<ShortcutInfo> shortcutInfoList) {
228     dynamicShortcuts.clear();
229     return addDynamicShortcuts(shortcutInfoList);
230   }
231 
232   @Implementation
updateShortcuts(List<ShortcutInfo> shortcutInfoList)233   protected boolean updateShortcuts(List<ShortcutInfo> shortcutInfoList) {
234     List<ShortcutInfo> existingShortcutsToUpdate = new ArrayList<>();
235     for (ShortcutInfo shortcutInfo : shortcutInfoList) {
236       if (dynamicShortcuts.containsKey(shortcutInfo.getId())
237           || activePinnedShortcuts.containsKey(shortcutInfo.getId())
238           || disabledPinnedShortcuts.containsKey(shortcutInfo.getId())) {
239         existingShortcutsToUpdate.add(shortcutInfo);
240       }
241     }
242     return addDynamicShortcuts(existingShortcutsToUpdate);
243   }
244 
245   /**
246    * No-op on Robolectric. The real implementation calls out to a service, which will NPE on
247    * Robolectric.
248    */
updateShortcutVisibility( final String packageName, final byte[] certificate, final boolean visible)249   protected void updateShortcutVisibility(
250       final String packageName, final byte[] certificate, final boolean visible) {}
251 
252   /**
253    * In Robolectric, ShadowShortcutManager doesn't perform any caching so long lived shortcuts are
254    * returned on place of shortcuts cached when shown in notifications.
255    */
256   @Implementation(minSdk = R)
getShortcuts(int matchFlags)257   protected List<ShortcutInfo> getShortcuts(int matchFlags) {
258     if (matchFlags == 0) {
259       return Lists.newArrayList();
260     }
261 
262     Set<ShortcutInfo> shortcutInfoSet = new HashSet<>();
263     shortcutInfoSet.addAll(getManifestShortcuts());
264     shortcutInfoSet.addAll(getDynamicShortcuts());
265     shortcutInfoSet.addAll(getPinnedShortcuts());
266 
267     return shortcutInfoSet.stream()
268         .filter(
269             shortcutInfo ->
270                 ((matchFlags & FLAG_MATCH_MANIFEST) != 0 && shortcutInfo.isDeclaredInManifest())
271                     || ((matchFlags & FLAG_MATCH_DYNAMIC) != 0 && shortcutInfo.isDynamic())
272                     || ((matchFlags & FLAG_MATCH_PINNED) != 0 && shortcutInfo.isPinned())
273                     || ((matchFlags & FLAG_MATCH_CACHED) != 0
274                         && (shortcutInfo.isCached() || shortcutInfo.isLongLived())))
275         .collect(toCollection(ArrayList::new));
276   }
277 
278   /**
279    * In Robolectric, ShadowShortcutManager doesn't handle rate limiting or shortcut count limits.
280    * So, pushDynamicShortcut is similar to {@link #addDynamicShortcuts(List)} but with only one
281    * {@link ShortcutInfo}.
282    */
283   @Implementation(minSdk = R)
pushDynamicShortcut(ShortcutInfo shortcut)284   protected void pushDynamicShortcut(ShortcutInfo shortcut) {
285     addDynamicShortcuts(Arrays.asList(shortcut));
286   }
287 
288   /**
289    * No-op on Robolectric. The real implementation calls out to a service, which will NPE on
290    * Robolectric.
291    */
292   @Implementation(minSdk = VERSION_CODES.R)
removeLongLivedShortcuts(List<String> shortcutIds)293   protected void removeLongLivedShortcuts(List<String> shortcutIds) {}
294 }
295