Home | History | Annotate | Download | only in shadows
      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