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