summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Matías Hernández <matiashe@google.com> 2023-12-05 15:35:36 +0100
committer Matías Hernández <matiashe@google.com> 2024-01-11 14:21:59 +0100
commit6480b8b78a8343274f53356d6edce2437a4f58da (patch)
tree5631e483d392517faf30fbd4c46185fc2fd83baf
parent65403b120556c2d3617565ac46fc42332c703633 (diff)
Preserve user customization when an app deletes and recreates an AutomaticZenRule
Test: atest ZenModeHelperTest Bug: 308672001 Change-Id: Iccbef610c50a062f627fdedca54881b8fd0264ed
-rw-r--r--core/java/android/service/notification/ZenModeConfig.java149
-rw-r--r--core/java/android/service/notification/ZenModeDiff.java10
-rwxr-xr-xservices/core/java/com/android/server/notification/NotificationManagerService.java6
-rw-r--r--services/core/java/com/android/server/notification/ZenModeHelper.java146
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java7
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java14
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java358
7 files changed, 614 insertions, 76 deletions
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 45a0c205a09b..54248be74e04 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -64,6 +64,7 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
@@ -233,6 +234,7 @@ public class ZenModeConfig implements Parcelable {
private static final String MANUAL_TAG = "manual";
private static final String AUTOMATIC_TAG = "automatic";
+ private static final String AUTOMATIC_DELETED_TAG = "deleted";
private static final String RULE_ATT_ID = "ruleId";
private static final String RULE_ATT_ENABLED = "enabled";
@@ -251,6 +253,7 @@ public class ZenModeConfig implements Parcelable {
private static final String RULE_ATT_USER_MODIFIED_FIELDS = "userModifiedFields";
private static final String RULE_ATT_ICON = "rule_icon";
private static final String RULE_ATT_TRIGGER_DESC = "triggerDesc";
+ private static final String RULE_ATT_DELETION_INSTANT = "deletionInstant";
private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -292,6 +295,10 @@ public class ZenModeConfig implements Parcelable {
@UnsupportedAppUsage
public ArrayMap<String, ZenRule> automaticRules = new ArrayMap<>();
+ // Note: Map is *pkg|conditionId* (see deletedRuleKey()) -> ZenRule,
+ // unlike automaticRules (which is id -> rule).
+ public final ArrayMap<String, ZenRule> deletedRules = new ArrayMap<>();
+
@UnsupportedAppUsage
public ZenModeConfig() {
}
@@ -306,15 +313,9 @@ public class ZenModeConfig implements Parcelable {
allowMessagesFrom = source.readInt();
user = source.readInt();
manualRule = source.readParcelable(null, ZenRule.class);
- final int len = source.readInt();
- if (len > 0) {
- final String[] ids = new String[len];
- final ZenRule[] rules = new ZenRule[len];
- source.readStringArray(ids);
- source.readTypedArray(rules, ZenRule.CREATOR);
- for (int i = 0; i < len; i++) {
- automaticRules.put(ids[i], rules[i]);
- }
+ readRulesFromParcel(automaticRules, source);
+ if (Flags.modesApi()) {
+ readRulesFromParcel(deletedRules, source);
}
allowAlarms = source.readInt() == 1;
allowMedia = source.readInt() == 1;
@@ -328,6 +329,19 @@ public class ZenModeConfig implements Parcelable {
}
}
+ private static void readRulesFromParcel(ArrayMap<String, ZenRule> ruleMap, Parcel source) {
+ final int len = source.readInt();
+ if (len > 0) {
+ final String[] ids = new String[len];
+ final ZenRule[] rules = new ZenRule[len];
+ source.readStringArray(ids);
+ source.readTypedArray(rules, ZenRule.CREATOR);
+ for (int i = 0; i < len; i++) {
+ ruleMap.put(ids[i], rules[i]);
+ }
+ }
+ }
+
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(allowCalls ? 1 : 0);
@@ -339,19 +353,9 @@ public class ZenModeConfig implements Parcelable {
dest.writeInt(allowMessagesFrom);
dest.writeInt(user);
dest.writeParcelable(manualRule, 0);
- if (!automaticRules.isEmpty()) {
- final int len = automaticRules.size();
- final String[] ids = new String[len];
- final ZenRule[] rules = new ZenRule[len];
- for (int i = 0; i < len; i++) {
- ids[i] = automaticRules.keyAt(i);
- rules[i] = automaticRules.valueAt(i);
- }
- dest.writeInt(len);
- dest.writeStringArray(ids);
- dest.writeTypedArray(rules, 0);
- } else {
- dest.writeInt(0);
+ writeRulesToParcel(automaticRules, dest);
+ if (Flags.modesApi()) {
+ writeRulesToParcel(deletedRules, dest);
}
dest.writeInt(allowAlarms ? 1 : 0);
dest.writeInt(allowMedia ? 1 : 0);
@@ -365,6 +369,23 @@ public class ZenModeConfig implements Parcelable {
}
}
+ private static void writeRulesToParcel(ArrayMap<String, ZenRule> ruleMap, Parcel dest) {
+ if (!ruleMap.isEmpty()) {
+ final int len = ruleMap.size();
+ final String[] ids = new String[len];
+ final ZenRule[] rules = new ZenRule[len];
+ for (int i = 0; i < len; i++) {
+ ids[i] = ruleMap.keyAt(i);
+ rules[i] = ruleMap.valueAt(i);
+ }
+ dest.writeInt(len);
+ dest.writeStringArray(ids);
+ dest.writeTypedArray(rules, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[')
@@ -389,23 +410,26 @@ public class ZenModeConfig implements Parcelable {
} else {
sb.append(",areChannelsBypassingDnd=").append(areChannelsBypassingDnd);
}
- return sb.append(",\nautomaticRules=").append(rulesToString())
- .append(",\nmanualRule=").append(manualRule)
- .append(']').toString();
+ sb.append(",\nautomaticRules=").append(rulesToString(automaticRules))
+ .append(",\nmanualRule=").append(manualRule);
+ if (Flags.modesApi()) {
+ sb.append(",\ndeletedRules=").append(rulesToString(deletedRules));
+ }
+ return sb.append(']').toString();
}
- private String rulesToString() {
- if (automaticRules.isEmpty()) {
+ private static String rulesToString(ArrayMap<String, ZenRule> ruleList) {
+ if (ruleList.isEmpty()) {
return "{}";
}
- StringBuilder buffer = new StringBuilder(automaticRules.size() * 28);
+ StringBuilder buffer = new StringBuilder(ruleList.size() * 28);
buffer.append("{\n");
- for (int i = 0; i < automaticRules.size(); i++) {
+ for (int i = 0; i < ruleList.size(); i++) {
if (i > 0) {
buffer.append(",\n");
}
- Object value = automaticRules.valueAt(i);
+ Object value = ruleList.valueAt(i);
buffer.append(value);
}
buffer.append('}');
@@ -487,7 +511,9 @@ public class ZenModeConfig implements Parcelable {
&& other.allowConversations == allowConversations
&& other.allowConversationsFrom == allowConversationsFrom;
if (Flags.modesApi()) {
- return eq && other.allowPriorityChannels == allowPriorityChannels;
+ return eq
+ && Objects.equals(other.deletedRules, deletedRules)
+ && other.allowPriorityChannels == allowPriorityChannels;
}
return eq;
}
@@ -644,12 +670,20 @@ public class ZenModeConfig implements Parcelable {
DEFAULT_SUPPRESSED_VISUAL_EFFECTS);
} else if (MANUAL_TAG.equals(tag)) {
rt.manualRule = readRuleXml(parser);
- } else if (AUTOMATIC_TAG.equals(tag)) {
+ } else if (AUTOMATIC_TAG.equals(tag)
+ || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) {
final String id = parser.getAttributeValue(null, RULE_ATT_ID);
final ZenRule automaticRule = readRuleXml(parser);
if (id != null && automaticRule != null) {
automaticRule.id = id;
- rt.automaticRules.put(id, automaticRule);
+ if (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag)) {
+ String deletedRuleKey = deletedRuleKey(automaticRule);
+ if (deletedRuleKey != null) {
+ rt.deletedRules.put(deletedRuleKey, automaticRule);
+ }
+ } else if (AUTOMATIC_TAG.equals(tag)) {
+ rt.automaticRules.put(id, automaticRule);
+ }
}
} else if (STATE_TAG.equals(tag)) {
rt.areChannelsBypassingDnd = safeBoolean(parser,
@@ -660,13 +694,24 @@ public class ZenModeConfig implements Parcelable {
throw new IllegalStateException("Failed to reach END_DOCUMENT");
}
+ /** Generates the map key used for a {@link ZenRule} in {@link #deletedRules}. */
+ @Nullable
+ public static String deletedRuleKey(ZenRule rule) {
+ if (rule.pkg != null && rule.conditionId != null) {
+ return rule.pkg + "|" + rule.conditionId.toString();
+ } else {
+ return null;
+ }
+ }
+
/**
* Writes XML of current ZenModeConfig
* @param out serializer
* @param version uses XML_VERSION if version is null
* @throws IOException
*/
- public void writeXml(TypedXmlSerializer out, Integer version) throws IOException {
+ public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup)
+ throws IOException {
out.startTag(null, ZEN_TAG);
out.attribute(null, ZEN_ATT_VERSION, version == null
? Integer.toString(XML_VERSION) : Integer.toString(version));
@@ -707,6 +752,15 @@ public class ZenModeConfig implements Parcelable {
writeRuleXml(automaticRule, out);
out.endTag(null, AUTOMATIC_TAG);
}
+ if (Flags.modesApi() && !forBackup) {
+ for (int i = 0; i < deletedRules.size(); i++) {
+ final ZenRule deletedRule = deletedRules.valueAt(i);
+ out.startTag(null, AUTOMATIC_DELETED_TAG);
+ out.attribute(null, RULE_ATT_ID, deletedRule.id);
+ writeRuleXml(deletedRule, out);
+ out.endTag(null, AUTOMATIC_DELETED_TAG);
+ }
+ }
out.startTag(null, STATE_TAG);
out.attributeBoolean(null, STATE_ATT_CHANNELS_BYPASSING_DND, areChannelsBypassingDnd);
@@ -752,6 +806,11 @@ public class ZenModeConfig implements Parcelable {
rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC);
rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN);
rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0);
+ Long deletionInstant = tryParseLong(
+ parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
+ if (deletionInstant != null) {
+ rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
+ }
}
return rt;
}
@@ -799,6 +858,10 @@ public class ZenModeConfig implements Parcelable {
}
out.attributeInt(null, RULE_ATT_TYPE, rule.type);
out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields);
+ if (rule.deletionInstant != null) {
+ out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
+ rule.deletionInstant.toEpochMilli());
+ }
}
}
@@ -1998,6 +2061,7 @@ public class ZenModeConfig implements Parcelable {
public String iconResName;
public boolean allowManualInvocation;
public int userModifiedFields;
+ @Nullable public Instant deletionInstant; // Only set on deleted rules.
public ZenRule() { }
@@ -2031,6 +2095,9 @@ public class ZenModeConfig implements Parcelable {
triggerDescription = source.readString();
type = source.readInt();
userModifiedFields = source.readInt();
+ if (source.readInt() == 1) {
+ deletionInstant = Instant.ofEpochMilli(source.readLong());
+ }
}
}
@@ -2091,6 +2158,12 @@ public class ZenModeConfig implements Parcelable {
dest.writeString(triggerDescription);
dest.writeInt(type);
dest.writeInt(userModifiedFields);
+ if (deletionInstant != null) {
+ dest.writeInt(1);
+ dest.writeLong(deletionInstant.toEpochMilli());
+ } else {
+ dest.writeInt(0);
+ }
}
}
@@ -2121,6 +2194,9 @@ public class ZenModeConfig implements Parcelable {
.append(",triggerDescription=").append(triggerDescription)
.append(",type=").append(type)
.append(",userModifiedFields=").append(userModifiedFields);
+ if (deletionInstant != null) {
+ sb.append(",deletionInstant=").append(deletionInstant);
+ }
}
return sb.append(']').toString();
@@ -2180,7 +2256,8 @@ public class ZenModeConfig implements Parcelable {
&& Objects.equals(other.iconResName, iconResName)
&& Objects.equals(other.triggerDescription, triggerDescription)
&& other.type == type
- && other.userModifiedFields == userModifiedFields;
+ && other.userModifiedFields == userModifiedFields
+ && Objects.equals(other.deletionInstant, deletionInstant);
}
return finalEquals;
@@ -2192,7 +2269,7 @@ public class ZenModeConfig implements Parcelable {
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy,
zenDeviceEffects, modified, allowManualInvocation, iconResName,
- triggerDescription, type, userModifiedFields);
+ triggerDescription, type, userModifiedFields, deletionInstant);
}
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy, modified);
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index 8902368072bf..91ef11cf1d2d 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -30,6 +30,11 @@ import java.util.Set;
/**
* ZenModeDiff is a utility class meant to encapsulate the diff between ZenModeConfigs and their
* subcomponents (automatic and manual ZenRules).
+ *
+ * <p>Note that this class is intended to detect <em>meaningful</em> differences, so objects that
+ * are not identical (as per their {@code equals()} implementation) can still produce an empty diff
+ * if only "metadata" fields are updated.
+ *
* @hide
*/
public class ZenModeDiff {
@@ -467,7 +472,6 @@ public class ZenModeDiff {
public static final String FIELD_ICON_RES = "iconResName";
public static final String FIELD_TRIGGER_DESCRIPTION = "triggerDescription";
public static final String FIELD_TYPE = "type";
- public static final String FIELD_USER_MODIFIED_FIELDS = "userModifiedFields";
// NOTE: new field strings must match the variable names in ZenModeConfig.ZenRule
// Special field to track whether this rule became active or inactive
@@ -563,10 +567,6 @@ public class ZenModeDiff {
if (!Objects.equals(from.iconResName, to.iconResName)) {
addField(FIELD_ICON_RES, new FieldDiff<>(from.iconResName, to.iconResName));
}
- if (from.userModifiedFields != to.userModifiedFields) {
- addField(FIELD_USER_MODIFIED_FIELDS,
- new FieldDiff<>(from.userModifiedFields, to.userModifiedFields));
- }
}
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 7aa7b7e1bfc1..9ddc362769f6 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -215,7 +215,6 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.LauncherApps;
-import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageManagerInternal;
@@ -373,6 +372,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
+import java.time.Clock;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@@ -2466,8 +2466,8 @@ public class NotificationManagerService extends SystemService {
mMetricsLogger = new MetricsLogger();
mRankingHandler = rankingHandler;
mConditionProviders = conditionProviders;
- mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), mConditionProviders,
- flagResolver, new ZenModeEventLogger(mPackageManagerClient));
+ mZenModeHelper = new ZenModeHelper(getContext(), mHandler.getLooper(), Clock.systemUTC(),
+ mConditionProviders, flagResolver, new ZenModeEventLogger(mPackageManagerClient));
mZenModeHelper.addCallback(new ZenModeHelper.Callback() {
@Override
public void onConfigChanged() {
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 911643b1a634..afbf08d9b77d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -117,6 +117,9 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.PrintWriter;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -130,9 +133,12 @@ public class ZenModeHelper {
static final String TAG = "ZenModeHelper";
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final String PACKAGE_ANDROID = "android";
+
// The amount of time rules instances can exist without their owning app being installed.
private static final int RULE_INSTANCE_GRACE_PERIOD = 1000 * 60 * 60 * 72;
static final int RULE_LIMIT_PER_PACKAGE = 100;
+ private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);
private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name
@@ -148,6 +154,7 @@ public class ZenModeHelper {
private final Context mContext;
private final H mHandler;
+ private final Clock mClock;
private final SettingsObserver mSettingsObserver;
private final AppOpsManager mAppOps;
private final NotificationManager mNotificationManager;
@@ -189,11 +196,13 @@ public class ZenModeHelper {
private String[] mPriorityOnlyDndExemptPackages;
- public ZenModeHelper(Context context, Looper looper, ConditionProviders conditionProviders,
+ public ZenModeHelper(Context context, Looper looper, Clock clock,
+ ConditionProviders conditionProviders,
SystemUiSystemPropertiesFlags.FlagResolver flagResolver,
ZenModeEventLogger zenModeEventLogger) {
mContext = context;
mHandler = new H(looper);
+ mClock = clock;
addCallback(mMetrics);
mAppOps = context.getSystemService(AppOpsManager.class);
mNotificationManager = context.getSystemService(NotificationManager.class);
@@ -452,6 +461,7 @@ public class ZenModeHelper {
newConfig = mConfig.copy();
ZenRule rule = new ZenRule();
populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true);
+ rule = maybeRestoreRemovedRule(newConfig, rule, automaticZenRule, origin);
newConfig.automaticRules.put(rule.id, rule);
maybeReplaceDefaultRule(newConfig, automaticZenRule);
@@ -463,6 +473,37 @@ public class ZenModeHelper {
}
}
+ private ZenRule maybeRestoreRemovedRule(ZenModeConfig config, ZenRule ruleToAdd,
+ AutomaticZenRule azrToAdd, @ConfigChangeOrigin int origin) {
+ if (!Flags.modesApi()) {
+ return ruleToAdd;
+ }
+ String deletedKey = ZenModeConfig.deletedRuleKey(ruleToAdd);
+ if (deletedKey == null) {
+ // Couldn't calculate the deletedRuleKey (condition or pkg null?). This should
+ // never happen for an app-provided rule because NMS validates both.
+ return ruleToAdd;
+ }
+ ZenRule ruleToRestore = config.deletedRules.get(deletedKey);
+ if (ruleToRestore == null) {
+ return ruleToAdd; // Cannot restore.
+ }
+
+ // We have found a previous rule to maybe restore. Whether we do that or not, we don't need
+ // to keep it around (if not restored now, it won't be in future calls either).
+ config.deletedRules.remove(deletedKey);
+ ruleToRestore.deletionInstant = null;
+
+ if (origin != UPDATE_ORIGIN_APP) {
+ return ruleToAdd; // Okay to create anew.
+ }
+
+ // "Preserve" the previous rule by considering the azrToAdd an update instead.
+ // Only app-modifiable fields will actually be modified.
+ populateZenRule(ruleToRestore.pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false);
+ return ruleToRestore;
+ }
+
private static void maybeReplaceDefaultRule(ZenModeConfig config, AutomaticZenRule addedRule) {
if (!Flags.modesApi()) {
return;
@@ -644,7 +685,7 @@ public class ZenModeHelper {
ZenRule rule = new ZenRule();
rule.id = implicitRuleId(pkg);
rule.pkg = pkg;
- rule.creationTime = System.currentTimeMillis();
+ rule.creationTime = mClock.millis();
Binder.withCleanCallingIdentity(() -> {
try {
@@ -664,7 +705,7 @@ public class ZenModeHelper {
rule.condition = null;
rule.conditionId = new Uri.Builder()
.scheme(Condition.SCHEME)
- .authority("android")
+ .authority(PACKAGE_ANDROID)
.appendPath("implicit")
.appendPath(pkg)
.build();
@@ -693,7 +734,9 @@ public class ZenModeHelper {
if (ruleToRemove == null) return false;
if (canManageAutomaticZenRule(ruleToRemove)) {
newConfig.automaticRules.remove(id);
- if (ruleToRemove.getPkg() != null && !"android".equals(ruleToRemove.getPkg())) {
+ maybePreserveRemovedRule(newConfig, ruleToRemove, origin);
+ if (ruleToRemove.getPkg() != null
+ && !PACKAGE_ANDROID.equals(ruleToRemove.getPkg())) {
for (ZenRule currRule : newConfig.automaticRules.values()) {
if (currRule.getPkg() != null
&& currRule.getPkg().equals(ruleToRemove.getPkg())) {
@@ -723,12 +766,44 @@ public class ZenModeHelper {
ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i));
if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) {
newConfig.automaticRules.removeAt(i);
+ maybePreserveRemovedRule(newConfig, rule, origin);
+ }
+ }
+ // If the system is clearing all rules this means DND access is revoked or the package
+ // was uninstalled, so also clear the preserved-deleted rules.
+ if (origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI) {
+ for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+ ZenRule rule = newConfig.deletedRules.get(newConfig.deletedRules.keyAt(i));
+ if (Objects.equals(rule.getPkg(), packageName)) {
+ newConfig.deletedRules.removeAt(i);
+ }
}
}
return setConfigLocked(newConfig, origin, reason, null, true, callingUid);
}
}
+ private void maybePreserveRemovedRule(ZenModeConfig config, ZenRule ruleToRemove,
+ @ConfigChangeOrigin int origin) {
+ if (!Flags.modesApi()) {
+ return;
+ }
+ // If an app deletes a previously customized rule, keep it around to preserve
+ // the user's customization when/if it's recreated later.
+ // We don't try to preserve system-owned rules because their conditionIds (used as
+ // deletedRuleKey) are not stable. This is almost moot anyway because an app cannot
+ // delete a system-owned rule.
+ if (origin == UPDATE_ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp()
+ && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
+ String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
+ if (deletedKey != null) {
+ ruleToRemove.deletionInstant = Instant.now(mClock);
+ // Overwrites a previously-deleted rule with the same conditionId, but that's okay.
+ config.deletedRules.put(deletedKey, ruleToRemove);
+ }
+ }
+ }
+
void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin,
int callingUid) {
ZenModeConfig newConfig;
@@ -919,7 +994,7 @@ public class ZenModeHelper {
// These values can always be edited by the app, so we apply changes immediately.
if (isNew) {
rule.id = ZenModeConfig.newRuleId();
- rule.creationTime = System.currentTimeMillis();
+ rule.creationTime = mClock.millis();
rule.component = automaticZenRule.getOwner();
rule.pkg = pkg;
}
@@ -1379,7 +1454,7 @@ public class ZenModeHelper {
boolean hasDefaultRules = config.automaticRules.containsAll(
ZenModeConfig.DEFAULT_RULE_IDS);
- long time = System.currentTimeMillis();
+ long time = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
if (config.automaticRules != null && config.automaticRules.size() > 0) {
for (ZenRule automaticRule : config.automaticRules.values()) {
if (forRestore) {
@@ -1419,6 +1494,12 @@ public class ZenModeHelper {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.ZEN_SETTINGS_UPDATED, 1, userId);
}
+
+ if (Flags.modesApi() && forRestore) {
+ // Note: forBackup doesn't write deletedRules, but just in case.
+ config.deletedRules.clear();
+ }
+
if (DEBUG) Log.d(TAG, reason);
synchronized (mConfigLock) {
setConfigLocked(config, null,
@@ -1436,7 +1517,7 @@ public class ZenModeHelper {
if (forBackup && mConfigs.keyAt(i) != userId) {
continue;
}
- mConfigs.valueAt(i).writeXml(out, version);
+ mConfigs.valueAt(i).writeXml(out, version, forBackup);
}
}
}
@@ -1468,28 +1549,51 @@ public class ZenModeHelper {
}
/**
- * Removes old rule instances whose owner is not installed.
+ * Cleans up obsolete rules:
+ * <ul>
+ * <li>Rule instances whose owner is not installed.
+ * <li>Deleted rules that were deleted more than 30 days ago.
+ * </ul>
*/
private void cleanUpZenRules() {
- long currentTime = System.currentTimeMillis();
+ Instant keptRuleThreshold = mClock.instant().minus(DELETED_RULE_KEPT_FOR);
synchronized (mConfigLock) {
final ZenModeConfig newConfig = mConfig.copy();
- if (newConfig.automaticRules != null) {
- for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) {
- ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i));
- if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) {
- try {
- if (rule.getPkg() != null) {
- mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER);
- }
- } catch (PackageManager.NameNotFoundException e) {
- newConfig.automaticRules.removeAt(i);
+
+ deleteRulesWithoutOwner(newConfig.automaticRules);
+ if (Flags.modesApi()) {
+ deleteRulesWithoutOwner(newConfig.deletedRules);
+ for (int i = newConfig.deletedRules.size() - 1; i >= 0; i--) {
+ ZenRule deletedRule = newConfig.deletedRules.valueAt(i);
+ if (deletedRule.deletionInstant == null
+ || deletedRule.deletionInstant.isBefore(keptRuleThreshold)) {
+ newConfig.deletedRules.removeAt(i);
+ }
+ }
+ }
+
+ if (!newConfig.equals(mConfig)) {
+ setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+ "cleanUpZenRules", Process.SYSTEM_UID);
+ }
+ }
+ }
+
+ private void deleteRulesWithoutOwner(ArrayMap<String, ZenRule> ruleList) {
+ long currentTime = Flags.modesApi() ? mClock.millis() : System.currentTimeMillis();
+ if (ruleList != null) {
+ for (int i = ruleList.size() - 1; i >= 0; i--) {
+ ZenRule rule = ruleList.valueAt(i);
+ if (RULE_INSTANCE_GRACE_PERIOD < (currentTime - rule.creationTime)) {
+ try {
+ if (rule.getPkg() != null) {
+ mPm.getPackageInfo(rule.getPkg(), PackageManager.MATCH_ANY_USER);
}
+ } catch (PackageManager.NameNotFoundException e) {
+ ruleList.removeAt(i);
}
}
}
- setConfigLocked(newConfig, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "cleanUpZenRules",
- Process.SYSTEM_UID);
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 177d64555899..dd252f3ffd20 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -60,6 +60,7 @@ import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.time.Instant;
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -407,6 +408,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.userModifiedFields = 16;
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
+ rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
Parcel parcel = Parcel.obtain();
rule.writeToParcel(parcel, 0);
@@ -432,9 +434,10 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.userModifiedFields, parceled.userModifiedFields);
assertEquals(rule.triggerDescription, parceled.triggerDescription);
assertEquals(rule.zenPolicy, parceled.zenPolicy);
+ assertEquals(rule.deletionInstant, parceled.deletionInstant);
+
assertEquals(rule, parceled);
assertEquals(rule.hashCode(), parceled.hashCode());
-
}
@Test
@@ -510,6 +513,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
rule.userModifiedFields = 4;
rule.iconResName = ICON_RES_NAME;
rule.triggerDescription = TRIGGER_DESC;
+ rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeRuleXml(rule, baos);
@@ -539,6 +543,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
assertEquals(rule.userModifiedFields, fromXml.userModifiedFields);
assertEquals(rule.triggerDescription, fromXml.triggerDescription);
assertEquals(rule.iconResName, fromXml.iconResName);
+ assertEquals(rule.deletionInstant, fromXml.deletionInstant);
}
@Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 7e92e427b9a4..9d7cf53e62db 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -65,18 +65,22 @@ import java.util.Set;
@TestableLooper.RunWithLooper
public class ZenModeDiffTest extends UiServiceTestCase {
// Base set of exempt fields independent of fields that are enabled/disabled via flags.
- // version is not included in the diff; manual & automatic rules have special handling
+ // version is not included in the diff; manual & automatic rules have special handling;
+ // deleted rules are not included in the diff.
public static final Set<String> ZEN_MODE_CONFIG_EXEMPT_FIELDS =
- Set.of("version", "manualRule", "automaticRules");
+ android.app.Flags.modesApi()
+ ? Set.of("version", "manualRule", "automaticRules", "deletedRules")
+ : Set.of("version", "manualRule", "automaticRules");
// Differences for flagged fields are only generated if the flag is enabled.
- // TODO: b/310620812 - Remove this exempt list when flag is inlined.
+ // "Metadata" fields (userModifiedFields, deletionInstant) are not compared.
private static final Set<String> ZEN_RULE_EXEMPT_FIELDS =
android.app.Flags.modesApi()
- ? Set.of()
+ ? Set.of("userModifiedFields", "deletionInstant")
: Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION,
RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL,
- RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, RuleDiff.FIELD_USER_MODIFIED_FIELDS);
+ RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields",
+ "deletionInstant");
// allowPriorityChannels is flagged by android.app.modes_api
public static final Set<String> ZEN_MODE_CONFIG_FLAGGED_FIELDS =
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 0224ff35219b..9e3e336fa12f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -43,6 +43,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
@@ -92,6 +93,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
+import android.Manifest;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.AppGlobals;
@@ -104,6 +106,7 @@ import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
@@ -116,6 +119,7 @@ import android.media.VolumePolicy;
import android.net.Uri;
import android.os.Parcel;
import android.os.Process;
+import android.os.SimpleClock;
import android.os.UserHandle;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
@@ -172,12 +176,16 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
@SmallTest
@SuppressLint("GuardedBy") // It's ok for this test to access guarded methods from the service.
@@ -232,6 +240,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Mock PackageManager mPackageManager;
private Resources mResources;
private TestableLooper mTestableLooper;
+ private final TestClock mTestClock = new TestClock();
private ZenModeHelper mZenModeHelper;
private ContentResolver mContentResolver;
@Mock DeviceEffectsApplier mDeviceEffectsApplier;
@@ -269,7 +278,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
mConditionProviders.addSystemProvider(new CountdownConditionProvider());
mConditionProviders.addSystemProvider(new ScheduleConditionProvider());
mZenModeEventLogger = new ZenModeEventLoggerFake(mPackageManager);
- mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(),
+ mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), mTestClock,
mConditionProviders, mTestFlagResolver, mZenModeEventLogger);
ResolveInfo ri = new ResolveInfo();
@@ -1198,7 +1207,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
@Test
public void ruleUidAutomaticZenRuleRemovedUpdatesCache() throws Exception {
when(mContext.checkCallingPermission(anyString()))
- .thenReturn(PackageManager.PERMISSION_GRANTED);
+ .thenReturn(PERMISSION_GRANTED);
setupZenConfig();
// one enabled automatic rule
@@ -1780,7 +1789,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
public void testDoNotUpdateModifiedDefaultAutoRule() {
// mDefaultConfig is set to default config in setup by getDefaultConfigParser
when(mContext.checkCallingPermission(anyString()))
- .thenReturn(PackageManager.PERMISSION_GRANTED);
+ .thenReturn(PERMISSION_GRANTED);
// shouldn't update rule that's been modified
ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
@@ -1806,7 +1815,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
public void testDoNotUpdateEnabledDefaultAutoRule() {
// mDefaultConfig is set to default config in setup by getDefaultConfigParser
when(mContext.checkCallingPermission(anyString()))
- .thenReturn(PackageManager.PERMISSION_GRANTED);
+ .thenReturn(PERMISSION_GRANTED);
// shouldn't update the rule that's enabled
ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
@@ -1833,7 +1842,7 @@ public class ZenModeHelperTest extends UiServiceTestCase {
// mDefaultConfig is set to default config in setup by getDefaultConfigParser
final String defaultRuleName = "rule name test";
when(mContext.checkCallingPermission(anyString()))
- .thenReturn(PackageManager.PERMISSION_GRANTED);
+ .thenReturn(PERMISSION_GRANTED);
// will update rule that is not enabled and modified
ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule();
@@ -4318,6 +4327,324 @@ public class ZenModeHelperTest extends UiServiceTestCase {
}
@Test
+ public void removeAndAddAutomaticZenRule_wasCustomized_isRestored() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+ // Start with a rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ mTestClock.setNowMillis(1000);
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+ assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue();
+
+ // User customizes it.
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+ Process.SYSTEM_UID);
+
+ // App deletes it.
+ mTestClock.advanceByMillis(1000);
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+ // App adds it again.
+ mTestClock.advanceByMillis(1000);
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // Verify that the rule was restored:
+ // - id and creation time is the same as the original one.
+ // - ZenPolicy is the one that the user had set.
+ // - rule still has the user-modified fields.
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000.
+ assertThat(newRuleId).isEqualTo(ruleId);
+ assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS);
+ assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo(
+ ZenPolicy.STATE_ALLOW);
+ assertThat(finalRule.getUserModifiedFields()).isEqualTo(
+ AutomaticZenRule.FIELD_INTERRUPTION_FILTER);
+ assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo(
+ ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS);
+
+ // Also, we discarded the "deleted rule" since we already used it for restoration.
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+ }
+
+ @Test
+ public void removeAndAddAutomaticZenRule_wasNotCustomized_isNotRestored() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+ // Start with a single rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ mTestClock.setNowMillis(1000);
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+ // App deletes it.
+ mTestClock.advanceByMillis(1000);
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+
+ // App adds it again.
+ mTestClock.advanceByMillis(1000);
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // Verify that the rule was recreated. This means id and creation time are new.
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getCreationTime()).isEqualTo(3000);
+ assertThat(newRuleId).isNotEqualTo(ruleId);
+ }
+
+ @Test
+ public void removeAndAddAutomaticZenRule_recreatedButNotByApp_isNotRestored() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+ // Start with a single rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ mTestClock.setNowMillis(1000);
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+ // User customizes it.
+ mTestClock.advanceByMillis(1000);
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+ Process.SYSTEM_UID);
+
+ // App deletes it.
+ mTestClock.advanceByMillis(1000);
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_APP, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1);
+
+ // User creates it again (unusual case, but ok).
+ mTestClock.advanceByMillis(1000);
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_USER, "add it anew", CUSTOM_PKG_UID);
+
+ // Verify that the rule was recreated. This means id and creation time are new, and the rule
+ // matches the latest data supplied to addAZR.
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getCreationTime()).isEqualTo(4000);
+ assertThat(newRuleId).isNotEqualTo(ruleId);
+ assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
+ assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo(
+ ZenPolicy.STATE_DISALLOW);
+
+ // Also, we discarded the "deleted rule" since we're not interested in recreating it.
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+ }
+
+ @Test
+ public void removeAndAddAutomaticZenRule_removedByUser_isNotRestored() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+
+ // Start with a single rule.
+ mZenModeHelper.mConfig.automaticRules.clear();
+ mTestClock.setNowMillis(1000);
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID)
+ .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build())
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000);
+
+ // User customizes it.
+ mTestClock.advanceByMillis(1000);
+ AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule)
+ .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
+ .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build())
+ .build();
+ mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, UPDATE_ORIGIN_USER, "userUpdate",
+ Process.SYSTEM_UID);
+
+ // User deletes it.
+ mTestClock.advanceByMillis(1000);
+ mZenModeHelper.removeAutomaticZenRule(ruleId, UPDATE_ORIGIN_USER, "delete it",
+ CUSTOM_PKG_UID);
+ assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0);
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0);
+
+ // App creates it again.
+ mTestClock.advanceByMillis(1000);
+ String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule,
+ UPDATE_ORIGIN_APP, "add it again", CUSTOM_PKG_UID);
+
+ // Verify that the rule was recreated. This means id and creation time are new.
+ AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId);
+ assertThat(finalRule.getCreationTime()).isEqualTo(4000);
+ assertThat(newRuleId).isNotEqualTo(ruleId);
+ }
+
+ @Test
+ public void removeAutomaticZenRule_preservedForRestoringByPackageAndConditionId() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+ mContext.getTestablePermissions().setPermission(Manifest.permission.MANAGE_NOTIFICATIONS,
+ PERMISSION_GRANTED); // So that canManageAZR passes although packages don't match.
+ mZenModeHelper.mConfig.automaticRules.clear();
+
+ // Start with a bunch of customized rules where conditionUris are not unique.
+ String id1 = mZenModeHelper.addAutomaticZenRule("pkg1",
+ new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ String id2 = mZenModeHelper.addAutomaticZenRule("pkg1",
+ new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ String id3 = mZenModeHelper.addAutomaticZenRule("pkg1",
+ new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ String id4 = mZenModeHelper.addAutomaticZenRule("pkg2",
+ new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ String id5 = mZenModeHelper.addAutomaticZenRule("pkg2",
+ new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) {
+ zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+ }
+
+ mZenModeHelper.removeAutomaticZenRule(id1, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+ mZenModeHelper.removeAutomaticZenRule(id2, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+ mZenModeHelper.removeAutomaticZenRule(id3, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+ mZenModeHelper.removeAutomaticZenRule(id4, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+ mZenModeHelper.removeAutomaticZenRule(id5, UPDATE_ORIGIN_APP, "begone", CUSTOM_PKG_UID);
+
+ assertThat(mZenModeHelper.mConfig.deletedRules.keySet())
+ .containsExactly("pkg1|uri1", "pkg1|uri2", "pkg2|uri1");
+ assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(zr -> zr.name)
+ .collect(Collectors.toList()))
+ .containsExactly("Test1", "Test3", "Test5");
+ }
+
+ @Test
+ public void removeAllZenRules_preservedForRestoring() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+ mZenModeHelper.mConfig.automaticRules.clear();
+
+ mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+ mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), UPDATE_ORIGIN_APP,
+ "add it", CUSTOM_PKG_UID);
+
+ for (ZenRule zenRule : mZenModeHelper.mConfig.automaticRules.values()) {
+ zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER;
+ }
+
+ mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), UPDATE_ORIGIN_APP,
+ "begone", CUSTOM_PKG_UID);
+
+ assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(2);
+ }
+
+ @Test
+ public void removeAllZenRules_fromSystem_deletesPreservedRulesToo() {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+ mZenModeHelper.mConfig.automaticRules.clear();
+
+ // Start with deleted rules from 2 different packages.
+ Instant now = Instant.ofEpochMilli(1701796461000L);
+ ZenRule pkg1Rule = newZenRule("pkg1", now.minus(1, ChronoUnit.DAYS), now);
+ ZenRule pkg2Rule = newZenRule("pkg2", now.minus(2, ChronoUnit.DAYS), now);
+ mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule);
+ mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule);
+
+ mZenModeHelper.removeAutomaticZenRules("pkg1",
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "goodbye pkg1", Process.SYSTEM_UID);
+
+ // Preserved rules from pkg1 are gone; those from pkg2 are still there.
+ assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(r -> r.pkg)
+ .collect(Collectors.toSet())).containsExactly("pkg2");
+ }
+
+ @Test
+ public void testRuleCleanup() throws Exception {
+ mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
+ Instant now = Instant.ofEpochMilli(1701796461000L);
+ Instant yesterday = now.minus(1, ChronoUnit.DAYS);
+ Instant aWeekAgo = now.minus(7, ChronoUnit.DAYS);
+ Instant twoMonthsAgo = now.minus(60, ChronoUnit.DAYS);
+ mTestClock.setNowMillis(now.toEpochMilli());
+
+ when(mPackageManager.getPackageInfo(eq("good_pkg"), anyInt()))
+ .thenReturn(new PackageInfo());
+ when(mPackageManager.getPackageInfo(eq("bad_pkg"), anyInt()))
+ .thenThrow(new PackageManager.NameNotFoundException("bad_pkg is not here"));
+
+ // Set up a config for another user containing:
+ ZenModeConfig config = new ZenModeConfig();
+ config.user = 42;
+ mZenModeHelper.mConfigs.put(42, config);
+ // okay rules (not deleted, package exists, with a range of creation dates).
+ config.automaticRules.put("ar1", newZenRule("good_pkg", now, null));
+ config.automaticRules.put("ar2", newZenRule("good_pkg", yesterday, null));
+ config.automaticRules.put("ar3", newZenRule("good_pkg", twoMonthsAgo, null));
+ // newish rules for a missing package
+ config.automaticRules.put("ar4", newZenRule("bad_pkg", yesterday, null));
+ // oldish rules belonging to a missing package
+ config.automaticRules.put("ar5", newZenRule("bad_pkg", aWeekAgo, null));
+ // rules deleted recently
+ config.deletedRules.put("del1", newZenRule("good_pkg", twoMonthsAgo, yesterday));
+ config.deletedRules.put("del2", newZenRule("good_pkg", twoMonthsAgo, aWeekAgo));
+ // rules deleted a long time ago
+ config.deletedRules.put("del3", newZenRule("good_pkg", twoMonthsAgo, twoMonthsAgo));
+ // rules for a missing package, created recently and deleted recently
+ config.deletedRules.put("del4", newZenRule("bad_pkg", yesterday, now));
+ // rules for a missing package, created a long time ago and deleted recently
+ config.deletedRules.put("del5", newZenRule("bad_pkg", twoMonthsAgo, now));
+ // rules for a missing package, created a long time ago and deleted a long time ago
+ config.deletedRules.put("del6", newZenRule("bad_pkg", twoMonthsAgo, twoMonthsAgo));
+
+ mZenModeHelper.onUserUnlocked(42); // copies config and cleans it up.
+
+ assertThat(mZenModeHelper.mConfig.automaticRules.keySet())
+ .containsExactly("ar1", "ar2", "ar3", "ar4");
+ assertThat(mZenModeHelper.mConfig.deletedRules.keySet())
+ .containsExactly("del1", "del2", "del4");
+ }
+
+ private static ZenRule newZenRule(String pkg, Instant createdAt, @Nullable Instant deletedAt) {
+ ZenRule rule = new ZenRule();
+ rule.pkg = pkg;
+ rule.creationTime = createdAt.toEpochMilli();
+ rule.deletionInstant = deletedAt;
+ // Plus stuff so that isValidAutomaticRule() passes
+ rule.name = "A rule from " + pkg + " created on " + createdAt;
+ rule.conditionId = Uri.parse(rule.name);
+ return rule;
+ }
+
+ @Test
public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
mZenModeHelper.mConfig.automaticRules.clear();
@@ -4919,4 +5246,25 @@ public class ZenModeHelperTest extends UiServiceTestCase {
return parser.nextTag();
}
}
+
+ private static class TestClock extends SimpleClock {
+ private long mNowMillis = 441644400000L;
+
+ private TestClock() {
+ super(ZoneOffset.UTC);
+ }
+
+ @Override
+ public long millis() {
+ return mNowMillis;
+ }
+
+ private void setNowMillis(long millis) {
+ mNowMillis = millis;
+ }
+
+ private void advanceByMillis(long millis) {
+ mNowMillis += millis;
+ }
+ }
}