blob: f38ba87ebe04a2b8bdb077f1319b10984eb704f5 [file] [log] [blame]
package org.robolectric.shadows;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import android.app.AutomaticZenRule;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.os.Build;
import android.os.Parcel;
import android.service.notification.StatusBarNotification;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.ReflectionHelpers;
@SuppressWarnings({"UnusedDeclaration"})
@Implements(value = NotificationManager.class, looseSignatures = true)
public class ShadowNotificationManager {
private boolean mAreNotificationsEnabled = true;
private boolean isNotificationPolicyAccessGranted = false;
private Map<Key, Notification> notifications = new HashMap<>();
private final Map<String, Object> notificationChannels = new HashMap<>();
private final Map<String, Object> notificationChannelGroups = new HashMap<>();
private final Map<String, Object> deletedNotificationChannels = new HashMap<>();
private final Map<String, AutomaticZenRule> automaticZenRules = new HashMap<>();
private int currentInteruptionFilter = INTERRUPTION_FILTER_ALL;
private Policy notificationPolicy;
@Implementation
protected void notify(int id, Notification notification) {
notify(null, id, notification);
}
@Implementation
protected void notify(String tag, int id, Notification notification) {
notifications.put(new Key(tag, id), notification);
}
@Implementation
protected void cancel(int id) {
cancel(null, id);
}
@Implementation
protected void cancel(String tag, int id) {
Key key = new Key(tag, id);
if (notifications.containsKey(key)) {
notifications.remove(key);
}
}
@Implementation
protected void cancelAll() {
notifications.clear();
}
@Implementation(minSdk = Build.VERSION_CODES.N)
protected boolean areNotificationsEnabled() {
return mAreNotificationsEnabled;
}
public void setNotificationsEnabled(boolean areNotificationsEnabled) {
mAreNotificationsEnabled = areNotificationsEnabled;
}
@Implementation(minSdk = M)
public StatusBarNotification[] getActiveNotifications() {
StatusBarNotification[] statusBarNotifications =
new StatusBarNotification[notifications.size()];
int i = 0;
for (Map.Entry<Key, Notification> entry : notifications.entrySet()) {
statusBarNotifications[i++] = new StatusBarNotification(
RuntimeEnvironment.application.getPackageName(),
null /* opPkg */,
entry.getKey().id,
entry.getKey().tag,
android.os.Process.myUid() /* uid */,
android.os.Process.myPid() /* initialPid */,
0 /* score */,
entry.getValue(),
android.os.Process.myUserHandle(),
0 /* postTime */);
}
return statusBarNotifications;
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected Object /*NotificationChannel*/ getNotificationChannel(String channelId) {
return notificationChannels.get(channelId);
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannelGroup(Object /*NotificationChannelGroup*/ group) {
String id = ReflectionHelpers.callInstanceMethod(group, "getId");
notificationChannelGroups.put(id, group);
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected List<Object /*NotificationChannelGroup*/> getNotificationChannelGroups() {
return ImmutableList.copyOf(notificationChannelGroups.values());
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannel(Object /*NotificationChannel*/ channel) {
String id = ReflectionHelpers.callInstanceMethod(channel, "getId");
// Per documentation, recreating a deleted channel should have the same settings as the old
// deleted channel. See
// https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannel%28java.lang.String%29
// for more info.
if (deletedNotificationChannels.containsKey(id)) {
notificationChannels.put(id, deletedNotificationChannels.remove(id));
} else {
notificationChannels.put(id, channel);
}
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannels(List<Object /*NotificationChannel*/> channelList) {
for (Object channel : channelList) {
createNotificationChannel(channel);
}
}
@Implementation(minSdk = Build.VERSION_CODES.O)
public List<Object /*NotificationChannel*/> getNotificationChannels() {
return ImmutableList.copyOf(notificationChannels.values());
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void deleteNotificationChannel(String channelId) {
if (getNotificationChannel(channelId) != null) {
Object /*NotificationChannel*/ channel = notificationChannels.remove(channelId);
deletedNotificationChannels.put(channelId, channel);
}
}
/**
* Delete a notification channel group and all notification channels associated with the group.
* This method will not notify any NotificationListenerService of resulting changes to
* notification channel groups nor to notification channels.
*/
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void deleteNotificationChannelGroup(String channelGroupId) {
if (getNotificationChannelGroup(channelGroupId) != null) {
// Deleting a channel group also deleted all associated channels. See
// https://developer.android.com/reference/android/app/NotificationManager.html#deleteNotificationChannelGroup%28java.lang.String%29
// for more info.
for (/* NotificationChannel */ Object channel : getNotificationChannels()) {
String groupId = ReflectionHelpers.callInstanceMethod(channel, "getGroup");
if (channelGroupId.equals(groupId)) {
String channelId = ReflectionHelpers.callInstanceMethod(channel, "getId");
deleteNotificationChannel(channelId);
}
}
notificationChannelGroups.remove(channelGroupId);
}
}
/**
* @return {@link NotificationManager#INTERRUPTION_FILTER_ALL} by default, or the value specified
* via {@link #setInterruptionFilter(int)}
*/
@Implementation(minSdk = M)
protected final int getCurrentInterruptionFilter() {
return currentInteruptionFilter;
}
/**
* Currently does not support checking for granted policy access.
*
* @see NotificationManager#getCurrentInterruptionFilter()
*/
@Implementation(minSdk = M)
protected final void setInterruptionFilter(int interruptionFilter) {
currentInteruptionFilter = interruptionFilter;
}
/**
* @return the value specified via {@link #setNotificationPolicy(Policy)}
*/
@Implementation(minSdk = M)
protected final Policy getNotificationPolicy() {
return notificationPolicy;
}
/**
* @return the value specified via {@link #setNotificationPolicyAccessGranted(boolean)}
*/
@Implementation(minSdk = M)
protected final boolean isNotificationPolicyAccessGranted() {
return isNotificationPolicyAccessGranted;
}
/**
* Currently does not support checking for granted policy access.
*
* @see NotificationManager#getNotificationPolicy()
*/
@Implementation(minSdk = M)
protected final void setNotificationPolicy(Policy policy) {
notificationPolicy = policy;
}
/**
* Sets the value returned by {@link NotificationManager#isNotificationPolicyAccessGranted()}. If
* {@code granted} is false, this also deletes all {@link AutomaticZenRule}s.
*
* @see NotificationManager#isNotificationPolicyAccessGranted()
*/
public void setNotificationPolicyAccessGranted(boolean granted) {
isNotificationPolicyAccessGranted = granted;
if (!granted) {
automaticZenRules.clear();
}
}
@Implementation(minSdk = N)
protected AutomaticZenRule getAutomaticZenRule(String id) {
Preconditions.checkNotNull(id);
enforcePolicyAccess();
return automaticZenRules.get(id);
}
@Implementation(minSdk = N)
protected Map<String, AutomaticZenRule> getAutomaticZenRules() {
enforcePolicyAccess();
ImmutableMap.Builder<String, AutomaticZenRule> rules = new ImmutableMap.Builder();
for (Map.Entry<String, AutomaticZenRule> entry : automaticZenRules.entrySet()) {
rules.put(entry.getKey(), copyAutomaticZenRule(entry.getValue()));
}
return rules.build();
}
@Implementation(minSdk = N)
protected String addAutomaticZenRule(AutomaticZenRule automaticZenRule) {
Preconditions.checkNotNull(automaticZenRule);
Preconditions.checkNotNull(automaticZenRule.getName());
Preconditions.checkNotNull(automaticZenRule.getOwner());
Preconditions.checkNotNull(automaticZenRule.getConditionId());
enforcePolicyAccess();
String id = UUID.randomUUID().toString().replace("-", "");
automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule));
return id;
}
@Implementation(minSdk = N)
protected boolean updateAutomaticZenRule(String id, AutomaticZenRule automaticZenRule) {
// NotificationManagerService doesn't check that id is non-null.
Preconditions.checkNotNull(automaticZenRule);
Preconditions.checkNotNull(automaticZenRule.getName());
Preconditions.checkNotNull(automaticZenRule.getOwner());
Preconditions.checkNotNull(automaticZenRule.getConditionId());
enforcePolicyAccess();
// ZenModeHelper throws slightly cryptic exceptions.
if (id == null) {
throw new IllegalArgumentException("Rule doesn't exist");
} else if (!automaticZenRules.containsKey(id)) {
throw new SecurityException("Cannot update rules not owned by your condition provider");
}
automaticZenRules.put(id, copyAutomaticZenRule(automaticZenRule));
return true;
}
@Implementation(minSdk = N)
protected boolean removeAutomaticZenRule(String id) {
Preconditions.checkNotNull(id);
enforcePolicyAccess();
return automaticZenRules.remove(id) != null;
}
/**
* Enforces that the caller has notification policy access.
*
* @see NotificationManager#isNotificationPolicyAccessGranted()
* @throws SecurityException if the caller doesn't have notification policy access
*/
private void enforcePolicyAccess() {
if (!isNotificationPolicyAccessGranted) {
throw new SecurityException("Notification policy access denied");
}
}
/** Returns a copy of {@code automaticZenRule}. */
private AutomaticZenRule copyAutomaticZenRule(AutomaticZenRule automaticZenRule) {
Parcel parcel = Parcel.obtain();
try {
automaticZenRule.writeToParcel(parcel, /* flags= */ 0);
parcel.setDataPosition(0);
return new AutomaticZenRule(parcel);
} finally {
parcel.recycle();
}
}
/**
* Checks whether a channel is considered a "deleted" channel by Android. This is a channel that
* was created but later deleted. If a channel is created that was deleted before, it recreates
* the channel with the old settings.
*/
public boolean isChannelDeleted(String channelId) {
return deletedNotificationChannels.containsKey(channelId);
}
public Object /*NotificationChannelGroup*/ getNotificationChannelGroup(String id) {
return notificationChannelGroups.get(id);
}
public int size() {
return notifications.size();
}
public Notification getNotification(int id) {
return notifications.get(new Key(null, id));
}
public Notification getNotification(String tag, int id) {
return notifications.get(new Key(tag, id));
}
public List<Notification> getAllNotifications() {
return new ArrayList<>(notifications.values());
}
private static final class Key {
public final String tag;
public final int id;
private Key(String tag, int id) {
this.tag = tag;
this.id = id;
}
@Override
public int hashCode() {
int hashCode = 17;
hashCode = 37 * hashCode + (tag == null ? 0 : tag.hashCode());
hashCode = 37 * hashCode + id;
return hashCode;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Key)) return false;
Key other = (Key) o;
return (this.tag == null ? other.tag == null : this.tag.equals(other.tag)) && this.id == other.id;
}
}
}