
org.robolectric.shadows.ShadowNotificationManager Maven / Gradle / Ivy
Show all versions of shadows-framework Show documentation
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 static android.os.Build.VERSION_CODES.O_MR1;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import android.annotation.NonNull;
import android.app.AutomaticZenRule;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.NotificationManager.Policy;
import android.content.ComponentName;
import android.os.Build;
import android.os.Parcel;
import android.service.notification.StatusBarNotification;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
/** Shadows for NotificationManager. */
@SuppressWarnings({"UnusedDeclaration", "AndroidConcurrentHashMap"})
@Implements(value = NotificationManager.class)
public class ShadowNotificationManager {
private static final int MAX_NOTIFICATION_LIMIT = 25;
private static boolean mAreNotificationsEnabled = true;
private static boolean isNotificationPolicyAccessGranted = false;
private static boolean enforceMaxNotificationLimit = false;
private static final Map notifications = new ConcurrentHashMap<>();
private static final Map notificationChannels =
new ConcurrentHashMap<>();
private static final Map notificationChannelGroups =
new ConcurrentHashMap<>();
private static final Map deletedNotificationChannels =
new ConcurrentHashMap<>();
private static final Map automaticZenRules = new ConcurrentHashMap<>();
private static final Map listenerAccessGrantedComponents =
new ConcurrentHashMap<>();
private static final Set canNotifyOnBehalfPackages = Sets.newConcurrentHashSet();
private static int currentInterruptionFilter = INTERRUPTION_FILTER_ALL;
private static Policy notificationPolicy;
private static Policy consolidatedNotificationPolicy;
private static String notificationDelegate;
private static int importance = NotificationManager.IMPORTANCE_NONE;
@Resetter
public static void reset() {
mAreNotificationsEnabled = true;
isNotificationPolicyAccessGranted = false;
enforceMaxNotificationLimit = false;
notifications.clear();
notificationChannels.clear();
notificationChannelGroups.clear();
deletedNotificationChannels.clear();
automaticZenRules.clear();
listenerAccessGrantedComponents.clear();
canNotifyOnBehalfPackages.clear();
currentInterruptionFilter = INTERRUPTION_FILTER_ALL;
notificationPolicy = null;
notificationDelegate = null;
consolidatedNotificationPolicy = null;
importance = NotificationManager.IMPORTANCE_NONE;
}
@Implementation
protected void notify(int id, Notification notification) {
notify(null, id, notification);
}
@Implementation
protected void notify(String tag, int id, Notification notification) {
if (!enforceMaxNotificationLimit || notifications.size() < MAX_NOTIFICATION_LIMIT) {
notifications.put(
new Key(tag, id), new PostedNotification(notification, ShadowSystem.currentTimeMillis()));
}
}
@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 = Build.VERSION_CODES.N)
protected int getImportance() {
return importance;
}
public void setImportance(int importance) {
this.importance = importance;
}
@Implementation(minSdk = M)
public StatusBarNotification[] getActiveNotifications() {
// Must make a copy because otherwise the size of the map may change after we have allocated
// the array:
ImmutableMap notifsCopy = ImmutableMap.copyOf(notifications);
StatusBarNotification[] statusBarNotifications = new StatusBarNotification[notifsCopy.size()];
int i = 0;
for (Map.Entry entry : notifsCopy.entrySet()) {
statusBarNotifications[i++] =
new StatusBarNotification(
RuntimeEnvironment.getApplication().getPackageName(),
null /* opPkg */,
entry.getKey().id,
entry.getKey().tag,
android.os.Process.myUid() /* uid */,
android.os.Process.myPid() /* initialPid */,
0 /* score */,
entry.getValue().notification,
android.os.Process.myUserHandle(),
entry.getValue().postedTimeMillis /* postTime */);
}
return statusBarNotifications;
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected NotificationChannel getNotificationChannel(String channelId) {
return notificationChannels.get(channelId);
}
/** Returns a NotificationChannel that has the given parent and conversation ID. */
@Implementation(minSdk = R)
protected NotificationChannel getNotificationChannel(String channelId, String conversationId) {
for (NotificationChannel notificationChannel : getNotificationChannels()) {
if (conversationId.equals(notificationChannel.getConversationId())
&& channelId.equals(notificationChannel.getParentChannelId())) {
return notificationChannel;
}
}
return null;
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannelGroup(NotificationChannelGroup group) {
String id = ReflectionHelpers.callInstanceMethod(group, "getId");
notificationChannelGroups.put(id, group);
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannelGroups(List groupList) {
for (NotificationChannelGroup group : groupList) {
createNotificationChannelGroup(group);
}
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected List getNotificationChannelGroups() {
return ImmutableList.copyOf(notificationChannelGroups.values());
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannel(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));
}
NotificationChannel existingChannel = (NotificationChannel) notificationChannels.get(id);
// Per documentation, recreating a channel can change name and description, lower importance or
// set a group if no group set. Other settings remain unchanged. See
// https://developer.android.com/reference/android/app/NotificationManager#createNotificationChannel%28android.app.NotificationChannel@29
// for more info.
if (existingChannel != null) {
NotificationChannel newChannel = (NotificationChannel) channel;
existingChannel.setName(newChannel.getName());
existingChannel.setDescription(newChannel.getDescription());
if (newChannel.getImportance() < existingChannel.getImportance()) {
existingChannel.setImportance(newChannel.getImportance());
}
if (Strings.isNullOrEmpty(existingChannel.getGroup())) {
existingChannel.setGroup(newChannel.getGroup());
}
return;
}
notificationChannels.put(id, channel);
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void createNotificationChannels(List channelList) {
for (NotificationChannel channel : channelList) {
createNotificationChannel(channel);
}
}
@Implementation(minSdk = Build.VERSION_CODES.O)
public List getNotificationChannels() {
return ImmutableList.copyOf(notificationChannels.values());
}
@Implementation(minSdk = Build.VERSION_CODES.O)
protected void deleteNotificationChannel(String channelId) {
if (getNotificationChannel(channelId) != null) {
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 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 int getCurrentInterruptionFilter() {
return currentInterruptionFilter;
}
/**
* Currently does not support checking for granted policy access.
*
* @see NotificationManager#getCurrentInterruptionFilter()
*/
@Implementation(minSdk = M)
protected void setInterruptionFilter(int interruptionFilter) {
currentInterruptionFilter = interruptionFilter;
}
/**
* @return the value specified via {@link #setNotificationPolicy(Policy)}
*/
@Implementation(minSdk = M)
protected Policy getNotificationPolicy() {
return notificationPolicy;
}
/**
* Specifies the consolidated notification policy to return
*
* @see #getConsolidatedNotificationPolicy()
*/
public void setConsolidatedNotificationPolicy(Policy policy) {
consolidatedNotificationPolicy = policy;
}
/**
* @return the value specified via {@link #setConsolidatedNotificationPolicy(Policy)}
*/
@Implementation(minSdk = R)
protected Policy getConsolidatedNotificationPolicy() {
return consolidatedNotificationPolicy;
}
/**
* @return the value specified via {@link #setNotificationPolicyAccessGranted(boolean)}
*/
@Implementation(minSdk = M)
protected boolean isNotificationPolicyAccessGranted() {
return isNotificationPolicyAccessGranted;
}
/**
* @return the value specified for the given {@link ComponentName} via {@link
* #setNotificationListenerAccessGranted(ComponentName, boolean)} or false if unset.
*/
@Implementation(minSdk = O_MR1)
protected boolean isNotificationListenerAccessGranted(ComponentName componentName) {
return listenerAccessGrantedComponents.getOrDefault(componentName.flattenToString(), false);
}
/**
* Currently does not support checking for granted policy access.
*
* @see NotificationManager#getNotificationPolicy()
*/
@Implementation(minSdk = M)
protected 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();
}
}
/**
* Sets the value returned by {@link
* NotificationManager#isNotificationListenerAccessGranted(ComponentName)} for the provided {@link
* ComponentName}.
*/
@Implementation(minSdk = O_MR1)
public void setNotificationListenerAccessGranted(ComponentName componentName, boolean granted) {
listenerAccessGrantedComponents.put(componentName.flattenToString(), granted);
}
@Implementation(minSdk = N)
protected AutomaticZenRule getAutomaticZenRule(String id) {
Preconditions.checkNotNull(id);
enforcePolicyAccess();
return automaticZenRules.get(id);
}
@Implementation(minSdk = N)
protected Map getAutomaticZenRules() {
enforcePolicyAccess();
ImmutableMap.Builder rules = new ImmutableMap.Builder();
for (Map.Entry 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.checkState(
automaticZenRule.getOwner() != null || automaticZenRule.getConfigurationActivity() != null,
"owner/configurationActivity cannot be null at the same time");
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.checkState(
automaticZenRule.getOwner() != null || automaticZenRule.getConfigurationActivity() != null,
"owner/configurationActivity cannot be null at the same time");
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;
}
@Implementation(minSdk = Q)
protected String getNotificationDelegate() {
return notificationDelegate;
}
@Implementation(minSdk = Q)
protected boolean canNotifyAsPackage(@NonNull String pkg) {
// TODO: This doesn't work correctly with notification delegates because
// ShadowNotificationManager doesn't respect the associated context, it just uses the global
// RuntimeEnvironment.getApplication() context.
// So for the sake of testing, we will compare with values set using
// setCanNotifyAsPackage()
return canNotifyOnBehalfPackages.contains(pkg);
}
/**
* Sets notification delegate for the package provided.
*
* {@link #canNotifyAsPackage(String)} will be returned based on this value.
*
* @param otherPackage the package for which the current package can notify on behalf
* @param canNotify whether the current package is set as notification delegate for 'otherPackage'
*/
public void setCanNotifyAsPackage(@NonNull String otherPackage, boolean canNotify) {
if (canNotify) {
canNotifyOnBehalfPackages.add(otherPackage);
} else {
canNotifyOnBehalfPackages.remove(otherPackage);
}
}
@Implementation(minSdk = Q)
protected void setNotificationDelegate(String delegate) {
notificationDelegate = delegate;
}
/**
* Ensures a notification limit is applied before posting the notification.
*
*
When set to true a maximum notification limit of 25 is applied. Notifications past this
* limit are dropped and are not posted or enqueued.
*
*
When set to false no limit is applied and all notifications are posted or enqueued. This is
* the default behavior.
*/
public void setEnforceMaxNotificationLimit(boolean enforceMaxNotificationLimit) {
this.enforceMaxNotificationLimit = enforceMaxNotificationLimit;
}
/**
* 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);
}
@Implementation(minSdk = P)
public NotificationChannelGroup getNotificationChannelGroup(String id) {
return notificationChannelGroups.get(id);
}
public int size() {
return notifications.size();
}
public Notification getNotification(int id) {
PostedNotification postedNotification = notifications.get(new Key(null, id));
return postedNotification == null ? null : postedNotification.notification;
}
public Notification getNotification(String tag, int id) {
PostedNotification postedNotification = notifications.get(new Key(tag, id));
return postedNotification == null ? null : postedNotification.notification;
}
public List getAllNotifications() {
List result = new ArrayList<>(notifications.size());
for (PostedNotification postedNotification : notifications.values()) {
result.add(postedNotification.notification);
}
return result;
}
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;
}
}
private static final class PostedNotification {
private final Notification notification;
private final long postedTimeMillis;
private PostedNotification(Notification notification, long postedTimeMillis) {
this.notification = notification;
this.postedTimeMillis = postedTimeMillis;
}
}
}