1 package org.robolectric.shadows; 2 3 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; 4 import static android.os.Build.VERSION_CODES.M; 5 import static android.os.Build.VERSION_CODES.N; 6 7 import android.app.AutomaticZenRule; 8 import android.app.Notification; 9 import android.app.NotificationManager; 10 import android.app.NotificationManager.Policy; 11 import android.os.Build; 12 import android.os.Parcel; 13 import android.service.notification.StatusBarNotification; 14 import com.google.common.base.Preconditions; 15 import com.google.common.collect.ImmutableList; 16 import com.google.common.collect.ImmutableMap; 17 import java.util.ArrayList; 18 import java.util.HashMap; 19 import java.util.List; 20 import java.util.Map; 21 import java.util.UUID; 22 import org.robolectric.RuntimeEnvironment; 23 import org.robolectric.annotation.Implementation; 24 import org.robolectric.annotation.Implements; 25 import org.robolectric.util.ReflectionHelpers; 26 27 @SuppressWarnings({"UnusedDeclaration"}) 28 @Implements(value = NotificationManager.class, looseSignatures = true) 29 public class ShadowNotificationManager { 30 private boolean mAreNotificationsEnabled = true; 31 private boolean isNotificationPolicyAccessGranted = false; 32 private Map<Key, Notification> notifications = new HashMap<>(); 33 private final Map<String, Object> notificationChannels = new HashMap<>(); 34 private final Map<String, Object> notificationChannelGroups = new HashMap<>(); 35 private final Map<String, Object> deletedNotificationChannels = new HashMap<>(); 36 private final Map<String, AutomaticZenRule> automaticZenRules = new HashMap<>(); 37 private int currentInteruptionFilter = INTERRUPTION_FILTER_ALL; 38 private Policy notificationPolicy; 39 40 @Implementation 41 protected void notify(int id, Notification notification) { 42 notify(null, id, notification); 43 } 44 45 @Implementation 46 protected void notify(String tag, int id, Notification notification) { 47 notifications.put(new Key(tag, id), notification); 48 } 49 50 @Implementation 51 protected void cancel(int id) { 52 cancel(null, id); 53 } 54 55 @Implementation 56 protected void cancel(String tag, int id) { 57 Key key = new Key(tag, id); 58 if (notifications.containsKey(key)) { 59 notifications.remove(key); 60 } 61 } 62 63 @Implementation 64 protected void cancelAll() { 65 notifications.clear(); 66 } 67 68 @Implementation(minSdk = Build.VERSION_CODES.N) 69 protected boolean areNotificationsEnabled() { 70 return mAreNotificationsEnabled; 71 } 72 73 public void setNotificationsEnabled(boolean areNotificationsEnabled) { 74 mAreNotificationsEnabled = areNotificationsEnabled; 75 } 76 77 @Implementation(minSdk = M) 78 public StatusBarNotification[] getActiveNotifications() { 79 StatusBarNotification[] statusBarNotifications = 80 new StatusBarNotification[notifications.size()]; 81 int i = 0; 82 for (Map.Entry<Key, Notification> entry : notifications.entrySet()) { 83 statusBarNotifications[i++] = new StatusBarNotification( 84 RuntimeEnvironment.application.getPackageName(), 85 null /* opPkg */, 86 entry.getKey().id, 87 entry.getKey().tag, 88 android.os.Process.myUid() /* uid */, 89 android.os.Process.myPid() /* initialPid */, 90 0 /* score */, 91 entry.getValue(), 92 android.os.Process.myUserHandle(), 93 0 /* postTime */); 94 } 95 return statusBarNotifications; 96 } 97 98 @Implementation(minSdk = Build.VERSION_CODES.O) 99 protected Object /*NotificationChannel*/ getNotificationChannel(String channelId) { 100 return notificationChannels.get(channelId); 101 } 102 103 @Implementation(minSdk = Build.VERSION_CODES.O) 104 protected void createNotificationChannelGroup(Object /*NotificationChannelGroup*/ group) { 105 String id = ReflectionHelpers.callInstanceMethod(group, "getId"); 106 notificationChannelGroups.put(id, group); 107 } 108 109 @Implementation(minSdk = Build.VERSION_CODES.O) 110 protected List<Object /*NotificationChannelGroup*/> getNotificationChannelGroups() { 111 return ImmutableList.copyOf(notificationChannelGroups.values()); 112 } 113 114 @Implementation(minSdk = Build.VERSION_CODES.O) 115 protected void createNotificationChannel(Object /*NotificationChannel*/ channel) { 116 String id = ReflectionHelpers.callInstanceMethod(channel, "getId"); 117 // Per documentation, recreating a deleted channel should have the same settings as the old 118 // deleted channel. See 119 // https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannel%28java.lang.String%29 120 // for more info. 121 if (deletedNotificationChannels.containsKey(id)) { 122 notificationChannels.put(id, deletedNotificationChannels.remove(id)); 123 } else { 124 notificationChannels.put(id, channel); 125 } 126 } 127 128 @Implementation(minSdk = Build.VERSION_CODES.O) 129 protected void createNotificationChannels(List<Object /*NotificationChannel*/> channelList) { 130 for (Object channel : channelList) { 131 createNotificationChannel(channel); 132 } 133 } 134 135 @Implementation(minSdk = Build.VERSION_CODES.O) 136 public List<Object /*NotificationChannel*/> getNotificationChannels() { 137 return ImmutableList.copyOf(notificationChannels.values()); 138 } 139 140 @Implementation(minSdk = Build.VERSION_CODES.O) 141 protected void deleteNotificationChannel(String channelId) { 142 if (getNotificationChannel(channelId) != null) { 143 Object /*NotificationChannel*/ channel = notificationChannels.remove(channelId); 144 deletedNotificationChannels.put(channelId, channel); 145 } 146 } 147 148 /** 149 * Delete a notification channel group and all notification channels associated with the group. 150 * This method will not notify any NotificationListenerService of resulting changes to 151 * notification channel groups nor to notification channels. 152 */ 153 @Implementation(minSdk = Build.VERSION_CODES.O) 154 protected void deleteNotificationChannelGroup(String channelGroupId) { 155 if (getNotificationChannelGroup(channelGroupId) != null) { 156 // Deleting a channel group also deleted all associated channels. See 157 // https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannelGroup%28java.lang.String%29 158 // for more info. 159 for (/* NotificationChannel */ Object channel : getNotificationChannels()) { 160 String groupId = ReflectionHelpers.callInstanceMethod(channel, "getGroup"); 161 if (channelGroupId.equals(groupId)) { 162 String channelId = ReflectionHelpers.callInstanceMethod(channel, "getId"); 163 deleteNotificationChannel(channelId); 164 } 165 } 166 notificationChannelGroups.remove(channelGroupId); 167 } 168 } 169 170 /** 171 * @return {@link NotificationManager#INTERRUPTION_FILTER_ALL} by default, or the value specified 172 * via {@link #setInterruptionFilter(int)} 173 */ 174 @Implementation(minSdk = M) 175 protected final int getCurrentInterruptionFilter() { 176 return currentInteruptionFilter; 177 } 178 179 /** 180 * Currently does not support checking for granted policy access. 181 * 182 * @see NotificationManager#getCurrentInterruptionFilter() 183 */ 184 @Implementation(minSdk = M) 185 protected final void setInterruptionFilter(int interruptionFilter) { 186 currentInteruptionFilter = interruptionFilter; 187 } 188 189 /** 190 * @return the value specified via {@link #setNotificationPolicy(Policy)} 191 */ 192 @Implementation(minSdk = M) 193 protected final Policy getNotificationPolicy() { 194 return notificationPolicy; 195 } 196 197 /** 198 * @return the value specified via {@link #setNotificationPolicyAccessGranted(boolean)} 199 */ 200 @Implementation(minSdk = M) 201 protected final boolean isNotificationPolicyAccessGranted() { 202 return isNotificationPolicyAccessGranted; 203 } 204 205 /** 206 * Currently does not support checking for granted policy access. 207 * 208 * @see NotificationManager#getNotificationPolicy() 209 */ 210 @Implementation(minSdk = M) 211 protected final void setNotificationPolicy(Policy policy) { 212 notificationPolicy = policy; 213 } 214 215 /** 216 * Sets the value returned by {@link NotificationManager#isNotificationPolicyAccessGranted()}. If 217 * {@code granted} is false, this also deletes all {@link AutomaticZenRule}s. 218 * 219 * @see NotificationManager#isNotificationPolicyAccessGranted() 220 */ 221 public void setNotificationPolicyAccessGranted(boolean granted) { 222 isNotificationPolicyAccessGranted = granted; 223 if (!granted) { 224 automaticZenRules.clear(); 225 } 226 } 227 228 @Implementation(minSdk = N) 229 protected AutomaticZenRule getAutomaticZenRule(String id) { 230 Preconditions.checkNotNull(id); 231 enforcePolicyAccess(); 232 233 return automaticZenRules.get(id); 234 } 235 236 @Implementation(minSdk = N) 237 protected Map<String, AutomaticZenRule> getAutomaticZenRules() { 238 enforcePolicyAccess(); 239 240 ImmutableMap.Builder<String, AutomaticZenRule> rules = new ImmutableMap.Builder(); 241 for (Map.Entry<String, AutomaticZenRule> entry : automaticZenRules.entrySet()) { 242 rules.put(entry.getKey(), copyAutomaticZenRule(entry.getValue())); 243 } 244 return rules.build(); 245 } 246 247 @Implementation(minSdk = N) 248 protected String addAutomaticZenRule(AutomaticZenRule automaticZenRule) { 249 Preconditions.checkNotNull(automaticZenRule); 250 Preconditions.checkNotNull(automaticZenRule.getName()); 251 Preconditions.checkNotNull(automaticZenRule.getOwner()); 252 Preconditions.checkNotNull(automaticZenRule.getConditionId()); 253 enforcePolicyAccess(); 254 255 String id = UUID.randomUUID().toString().replace("-", ""); 256 automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule)); 257 return id; 258 } 259 260 @Implementation(minSdk = N) 261 protected boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) { 262 // NotificationManagerService doesn't check that id is non-null. 263 Preconditions.checkNotNull(automaticZenRule); 264 Preconditions.checkNotNull(automaticZenRule.getName()); 265 Preconditions.checkNotNull(automaticZenRule.getOwner()); 266 Preconditions.checkNotNull(automaticZenRule.getConditionId()); 267 enforcePolicyAccess(); 268 269 // ZenModeHelper throws slightly cryptic exceptions. 270 if (id == null) { 271 throw new IllegalArgumentException("Rule doesn't exist"); 272 } else if (!automaticZenRules.containsKey(id)) { 273 throw new SecurityException("Cannot update rules not owned by your condition provider"); 274 } 275 276 automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule)); 277 return true; 278 } 279 280 @Implementation(minSdk = N) 281 protected boolean removeAutomaticZenRule(String id) { 282 Preconditions.checkNotNull(id); 283 enforcePolicyAccess(); 284 return automaticZenRules.remove(id) != null; 285 } 286 287 /** 288 * Enforces that the caller has notification policy access. 289 * 290 * @see NotificationManager#isNotificationPolicyAccessGranted() 291 * @throws SecurityException if the caller doesn't have notification policy access 292 */ 293 private void enforcePolicyAccess() { 294 if (!isNotificationPolicyAccessGranted) { 295 throw new SecurityException("Notification policy access denied"); 296 } 297 } 298 299 /** Returns a copy of {@code automaticZenRule}. */ 300 private AutomaticZenRule copyAutomaticZenRule(AutomaticZenRule automaticZenRule) { 301 Parcel parcel = Parcel.obtain(); 302 try { 303 automaticZenRule.writeToParcel(parcel, /* flags= */ 0); 304 parcel.setDataPosition(0); 305 return new AutomaticZenRule(parcel); 306 } finally { 307 parcel.recycle(); 308 } 309 } 310 311 /** 312 * Checks whether a channel is considered a "deleted" channel by Android. This is a channel that 313 * was created but later deleted. If a channel is created that was deleted before, it recreates 314 * the channel with the old settings. 315 */ 316 public boolean isChannelDeleted(String channelId) { 317 return deletedNotificationChannels.containsKey(channelId); 318 } 319 320 public Object /*NotificationChannelGroup*/ getNotificationChannelGroup(String id) { 321 return notificationChannelGroups.get(id); 322 } 323 324 public int size() { 325 return notifications.size(); 326 } 327 328 public Notification getNotification(int id) { 329 return notifications.get(new Key(null, id)); 330 } 331 332 public Notification getNotification(String tag, int id) { 333 return notifications.get(new Key(tag, id)); 334 } 335 336 public List<Notification> getAllNotifications() { 337 return new ArrayList<>(notifications.values()); 338 } 339 340 private static final class Key { 341 public final String tag; 342 public final int id; 343 344 private Key(String tag, int id) { 345 this.tag = tag; 346 this.id = id; 347 } 348 349 @Override 350 public int hashCode() { 351 int hashCode = 17; 352 hashCode = 37 * hashCode + (tag == null ? 0 : tag.hashCode()); 353 hashCode = 37 * hashCode + id; 354 return hashCode; 355 } 356 357 @Override 358 public boolean equals(Object o) { 359 if (!(o instanceof Key)) return false; 360 Key other = (Key) o; 361 return (this.tag == null ? other.tag == null : this.tag.equals(other.tag)) && this.id == other.id; 362 } 363 } 364 } 365