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