Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit dfce623b authored by Matías Hernández's avatar Matías Hernández
Browse files

Remove long unused (and non-user-modified) implicit ZenRules

We now track the last time each ZenRule was activated. On ZenModeConfig load (e.g. reboot, user switch) if 30 days have passed since the last activation of an implicit rule, it is deleted. For rules created before we started tracking activation, we also set lastActivation=now -- in essence, so that we wait 30 days before deciding whether they are actually used or not.

As a side-change, rename ZenRule.canBeUpdatedByApp() to ZenRule.isUserModified(), inverting is logic. The latter is the condition that almost all callsites are interested in.

Bug: 394087495
Test: atest ZenModeConfigTest ZenModeHelperTest
Flag: android.app.modes_cleanup_implicit
Change-Id: If9f7916e59b47871f14bd4b1b4efaf6cd2d41af7
parent 6dd098ac
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -82,6 +82,13 @@ flag {
  }
}

flag {
  name: "modes_cleanup_implicit"
  namespace: "systemui"
  description: "Deletes implicit modes if never customized and not used for some time. Depends on MODES_UI"
  bug: "394087495"
}

flag {
  name: "api_tvextender"
  is_exported: true
+86 −24
Original line number Diff line number Diff line
@@ -309,6 +309,7 @@ public class ZenModeConfig implements Parcelable {
    private static final String RULE_ATT_DISABLED_ORIGIN = "disabledOrigin";
    private static final String RULE_ATT_LEGACY_SUPPRESSED_EFFECTS = "legacySuppressedEffects";
    private static final String RULE_ATT_CONDITION_OVERRIDE = "conditionOverride";
    private static final String RULE_ATT_LAST_ACTIVATION = "lastActivation";

    private static final String DEVICE_EFFECT_DISPLAY_GRAYSCALE = "zdeDisplayGrayscale";
    private static final String DEVICE_EFFECT_SUPPRESS_AMBIENT_DISPLAY =
@@ -1187,11 +1188,7 @@ public class ZenModeConfig implements Parcelable {
        rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0);
        rt.zenDeviceEffectsUserModifiedFields = safeInt(parser,
                DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0);
        Long deletionInstant = tryParseLong(
                parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null);
        if (deletionInstant != null) {
            rt.deletionInstant = Instant.ofEpochMilli(deletionInstant);
        }
        rt.deletionInstant = safeInstant(parser, RULE_ATT_DELETION_INSTANT, null);
        if (Flags.modesUi()) {
            rt.disabledOrigin = safeInt(parser, RULE_ATT_DISABLED_ORIGIN,
                    ORIGIN_UNKNOWN);
@@ -1199,6 +1196,9 @@ public class ZenModeConfig implements Parcelable {
                    RULE_ATT_LEGACY_SUPPRESSED_EFFECTS, 0);
            rt.conditionOverride = safeInt(parser, RULE_ATT_CONDITION_OVERRIDE,
                    ZenRule.OVERRIDE_NONE);
            if (Flags.modesCleanupImplicit()) {
                rt.lastActivation = safeInstant(parser, RULE_ATT_LAST_ACTIVATION, null);
            }
        }

        return rt;
@@ -1249,10 +1249,7 @@ public class ZenModeConfig implements Parcelable {
        out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields);
        out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS,
                rule.zenDeviceEffectsUserModifiedFields);
        if (rule.deletionInstant != null) {
            out.attributeLong(null, RULE_ATT_DELETION_INSTANT,
                    rule.deletionInstant.toEpochMilli());
        }
        writeXmlAttributeInstant(out, RULE_ATT_DELETION_INSTANT, rule.deletionInstant);
        if (Flags.modesUi()) {
            out.attributeInt(null, RULE_ATT_DISABLED_ORIGIN, rule.disabledOrigin);
            out.attributeInt(null, RULE_ATT_LEGACY_SUPPRESSED_EFFECTS,
@@ -1260,6 +1257,16 @@ public class ZenModeConfig implements Parcelable {
            if (rule.conditionOverride == ZenRule.OVERRIDE_ACTIVATE && !forBackup) {
                out.attributeInt(null, RULE_ATT_CONDITION_OVERRIDE, rule.conditionOverride);
            }
            if (Flags.modesCleanupImplicit()) {
                writeXmlAttributeInstant(out, RULE_ATT_LAST_ACTIVATION, rule.lastActivation);
            }
        }
    }

    private static void writeXmlAttributeInstant(TypedXmlSerializer out, String att,
            @Nullable Instant instant) throws IOException {
        if (instant != null) {
            out.attributeLong(null, att, instant.toEpochMilli());
        }
    }

@@ -1600,6 +1607,19 @@ public class ZenModeConfig implements Parcelable {
        return values;
    }

    @Nullable
    private static Instant safeInstant(TypedXmlPullParser parser, String att,
            @Nullable Instant defValue) {
        final String strValue = parser.getAttributeValue(null, att);
        if (!TextUtils.isEmpty(strValue)) {
            Long longValue = tryParseLong(strValue, null);
            if (longValue != null) {
                return Instant.ofEpochMilli(longValue);
            }
        }
        return defValue;
    }

    @Override
    public int describeContents() {
        return 0;
@@ -2598,6 +2618,18 @@ public class ZenModeConfig implements Parcelable {
        @ConditionOverride
        int conditionOverride = OVERRIDE_NONE;

        /**
         * Last time at which the rule was activated (for any reason, including overrides).
         * If {@code null}, the rule has never been activated since its creation.
         *
         * <p>Note that this was previously untracked, so it will also be {@code null} for rules
         * created before we started tracking and never activated since -- make sure to account for
         * it, for example by falling back to {@link #creationTime} in logic involving this field.
         */
        @Nullable
        @FlaggedApi(Flags.FLAG_MODES_CLEANUP_IMPLICIT)
        public Instant lastActivation;

        public ZenRule() { }

        public ZenRule(Parcel source) {
@@ -2635,24 +2667,29 @@ public class ZenModeConfig implements Parcelable {
                disabledOrigin = source.readInt();
                legacySuppressedEffects = source.readInt();
                conditionOverride = source.readInt();
                if (Flags.modesCleanupImplicit()) {
                    if (source.readInt() == 1) {
                        lastActivation = Instant.ofEpochMilli(source.readLong());
                    }
                }
            }
        }

        /**
         * Whether this ZenRule can be updated by an app. In general, rules that have been
         * customized by the user cannot be further updated by an app, with some exceptions:
         * Whether this ZenRule has been customized by the user in any way.

         * <p>In general, rules that have been customized by the user cannot be further updated by
         * an app, with some exceptions:
         * <ul>
         *     <li>Non user-configurable fields, like type, icon, configurationActivity, etc.
         *     <li>Name, if the name was not specifically modified by the user (to support language
         *          switches).
         * </ul>
         */
        public boolean canBeUpdatedByApp() {
            // The rule is considered updateable if its bitmask has no user modifications, and
            // the bitmasks of the policy and device effects have no modification.
            return userModifiedFields == 0
                    && zenPolicyUserModifiedFields == 0
                    && zenDeviceEffectsUserModifiedFields == 0;
        public boolean isUserModified() {
            return userModifiedFields != 0
                    || zenPolicyUserModifiedFields != 0
                    || zenDeviceEffectsUserModifiedFields != 0;
        }

        @Override
@@ -2708,6 +2745,14 @@ public class ZenModeConfig implements Parcelable {
                dest.writeInt(disabledOrigin);
                dest.writeInt(legacySuppressedEffects);
                dest.writeInt(conditionOverride);
                if (Flags.modesCleanupImplicit()) {
                    if (lastActivation != null) {
                        dest.writeInt(1);
                        dest.writeLong(lastActivation.toEpochMilli());
                    } else {
                        dest.writeInt(0);
                    }
                }
            }
        }

@@ -2760,6 +2805,9 @@ public class ZenModeConfig implements Parcelable {
            if (Flags.modesUi()) {
                sb.append(",disabledOrigin=").append(disabledOrigin);
                sb.append(",legacySuppressedEffects=").append(legacySuppressedEffects);
                if (Flags.modesCleanupImplicit()) {
                    sb.append(",lastActivation=").append(lastActivation);
                }
            }

            return sb.append(']').toString();
@@ -2838,6 +2886,10 @@ public class ZenModeConfig implements Parcelable {
                        && other.disabledOrigin == disabledOrigin
                        && other.legacySuppressedEffects == legacySuppressedEffects
                        && other.conditionOverride == conditionOverride;
                if (Flags.modesCleanupImplicit()) {
                    finalEquals = finalEquals
                            && Objects.equals(other.lastActivation, lastActivation);
                }
            }

            return finalEquals;
@@ -2846,6 +2898,15 @@ public class ZenModeConfig implements Parcelable {
        @Override
        public int hashCode() {
            if (Flags.modesUi()) {
                if (Flags.modesCleanupImplicit()) {
                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                            component, configurationActivity, pkg, id, enabler, zenPolicy,
                            zenDeviceEffects, allowManualInvocation, iconResName,
                            triggerDescription, type, userModifiedFields,
                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
                            deletionInstant, disabledOrigin, legacySuppressedEffects,
                            conditionOverride, lastActivation);
                } else {
                    return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                            component, configurationActivity, pkg, id, enabler, zenPolicy,
                            zenDeviceEffects, allowManualInvocation, iconResName,
@@ -2853,6 +2914,7 @@ public class ZenModeConfig implements Parcelable {
                            zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
                            deletionInstant, disabledOrigin, legacySuppressedEffects,
                            conditionOverride);
                }
            } else {
                return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
                        component, configurationActivity, pkg, id, enabler, zenPolicy,
+51 −3
Original line number Diff line number Diff line
@@ -157,6 +157,12 @@ public class ZenModeHelper {
    static final int RULE_LIMIT_PER_PACKAGE = 100;
    private static final Duration DELETED_RULE_KEPT_FOR = Duration.ofDays(30);

    /**
     * Amount of time since last activation after which implicit rules that have never been
     * customized by the user are automatically cleaned up.
     */
    private static final Duration IMPLICIT_RULE_KEPT_FOR = Duration.ofDays(30);

    private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;

    /**
@@ -534,7 +540,7 @@ public class ZenModeHelper {
                    ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
            if (sleepingRule != null
                    && !sleepingRule.enabled
                    && sleepingRule.canBeUpdatedByApp() /* meaning it's not user-customized */) {
                    && !sleepingRule.isUserModified()) {
                config.automaticRules.remove(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
            }
        }
@@ -864,7 +870,7 @@ public class ZenModeHelper {
        // 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 == ORIGIN_APP && !ruleToRemove.canBeUpdatedByApp()
        if (origin == ORIGIN_APP && ruleToRemove.isUserModified()
                && !PACKAGE_ANDROID.equals(ruleToRemove.pkg)) {
            String deletedKey = ZenModeConfig.deletedRuleKey(ruleToRemove);
            if (deletedKey != null) {
@@ -1282,7 +1288,7 @@ public class ZenModeHelper {
        // * the request comes from an origin that can always update values, like the user, or
        // * the rule has not yet been user modified, and thus can be updated by the app.
        boolean updateValues = isNew || doesOriginAlwaysUpdateValues(origin)
                || rule.canBeUpdatedByApp();
                || !rule.isUserModified();

        // For all other values, if updates are not allowed, we discard the update.
        if (!updateValues) {
@@ -1914,6 +1920,7 @@ public class ZenModeHelper {
     * <ul>
     *     <li>Rule instances whose owner is not installed.
     *     <li>Deleted rules that were deleted more than 30 days ago.
     *     <li>Implicit rules that haven't been used in 30 days (and have not been customized).
     * </ul>
     */
    private void cleanUpZenRules() {
@@ -1932,6 +1939,10 @@ public class ZenModeHelper {
                }
            }

            if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
                deleteUnusedImplicitRules(newConfig.automaticRules);
            }

            if (!newConfig.equals(mConfig)) {
                setConfigLocked(newConfig, null, ORIGIN_SYSTEM,
                        "cleanUpZenRules", Process.SYSTEM_UID);
@@ -1957,6 +1968,29 @@ public class ZenModeHelper {
        }
    }

    private void deleteUnusedImplicitRules(ArrayMap<String, ZenRule> ruleList) {
        if (ruleList == null) {
            return;
        }
        Instant deleteIfUnusedSince = mClock.instant().minus(IMPLICIT_RULE_KEPT_FOR);

        for (int i = ruleList.size() - 1; i >= 0; i--) {
            ZenRule rule = ruleList.valueAt(i);
            if (isImplicitRuleId(rule.id) && !rule.isUserModified()) {
                if (rule.lastActivation == null) {
                    // This rule existed before we started tracking activation time. It *might* be
                    // in use. Set lastActivation=now so it has some time (IMPLICIT_RULE_KEPT_FOR)
                    // before being removed if truly unused.
                    rule.lastActivation = mClock.instant();
                }

                if (rule.lastActivation.isBefore(deleteIfUnusedSince)) {
                    ruleList.removeAt(i);
                }
            }
        }
    }

    /**
     * @return a copy of the zen mode configuration
     */
@@ -2091,6 +2125,20 @@ public class ZenModeHelper {
                }
            }

            // Update last activation for rules that are being activated.
            if (Flags.modesUi() && Flags.modesCleanupImplicit()) {
                Instant now = mClock.instant();
                if (!mConfig.isManualActive() && config.isManualActive()) {
                    config.manualRule.lastActivation = now;
                }
                for (ZenRule rule : config.automaticRules.values()) {
                    ZenRule previousRule = mConfig.automaticRules.get(rule.id);
                    if (rule.isActive() && (previousRule == null || !previousRule.isActive())) {
                        rule.lastActivation = now;
                    }
                }
            }

            mConfig = config;
            dispatchOnConfigChanged();
            updateAndApplyConsolidatedPolicyAndDeviceEffects(origin, reason);
+26 −8
Original line number Diff line number Diff line
@@ -488,33 +488,33 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
        rule.zenPolicy = null;
        rule.zenDeviceEffects = null;
        assertThat(rule.canBeUpdatedByApp()).isTrue();
        assertThat(rule.isUserModified()).isFalse();

        rule.userModifiedFields = 1;

        assertThat(rule.canBeUpdatedByApp()).isFalse();
        assertThat(rule.isUserModified()).isTrue();
    }

    @Test
    public void testCanBeUpdatedByApp_policyModified() throws Exception {
        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
        rule.zenPolicy = new ZenPolicy();
        assertThat(rule.canBeUpdatedByApp()).isTrue();
        assertThat(rule.isUserModified()).isFalse();

        rule.zenPolicyUserModifiedFields = 1;

        assertThat(rule.canBeUpdatedByApp()).isFalse();
        assertThat(rule.isUserModified()).isTrue();
    }

    @Test
    public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception {
        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
        rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build();
        assertThat(rule.canBeUpdatedByApp()).isTrue();
        assertThat(rule.isUserModified()).isFalse();

        rule.zenDeviceEffectsUserModifiedFields = 1;

        assertThat(rule.canBeUpdatedByApp()).isFalse();
        assertThat(rule.isUserModified()).isTrue();
    }

    @Test
@@ -563,6 +563,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
        if (Flags.modesUi()) {
            rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
            if (Flags.modesCleanupImplicit()) {
                rule.lastActivation = Instant.ofEpochMilli(456);
            }
        }
        config.automaticRules.put(rule.id, rule);

@@ -600,6 +603,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        assertEquals(rule.deletionInstant, ruleActual.deletionInstant);
        if (Flags.modesUi()) {
            assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin);
            if (Flags.modesCleanupImplicit()) {
                assertEquals(rule.lastActivation, ruleActual.lastActivation);
            }
        }
        if (Flags.backupRestoreLogging()) {
            verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2);
@@ -633,6 +639,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
        if (Flags.modesUi()) {
            rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
            if (Flags.modesCleanupImplicit()) {
                rule.lastActivation = Instant.ofEpochMilli(789);
            }
        }

        Parcel parcel = Parcel.obtain();
@@ -664,6 +673,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        assertEquals(rule.deletionInstant, parceled.deletionInstant);
        if (Flags.modesUi()) {
            assertEquals(rule.disabledOrigin, parceled.disabledOrigin);
            if (Flags.modesCleanupImplicit()) {
                assertEquals(rule.lastActivation, parceled.lastActivation);
            }
        }

        assertEquals(rule, parceled);
@@ -746,6 +758,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        rule.deletionInstant = Instant.ofEpochMilli(1701790147000L);
        if (Flags.modesUi()) {
            rule.disabledOrigin = ZenModeConfig.ORIGIN_APP;
            if (Flags.modesCleanupImplicit()) {
                rule.lastActivation = Instant.ofEpochMilli(123);
            }
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -781,6 +796,9 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        assertEquals(rule.deletionInstant, fromXml.deletionInstant);
        if (Flags.modesUi()) {
            assertEquals(rule.disabledOrigin, fromXml.disabledOrigin);
            if (Flags.modesCleanupImplicit()) {
                assertEquals(rule.lastActivation, fromXml.lastActivation);
            }
        }
    }

@@ -908,7 +926,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
        rule.userModifiedFields |= AutomaticZenRule.FIELD_NAME;
        assertThat(rule.userModifiedFields).isEqualTo(1);
        assertThat(rule.canBeUpdatedByApp()).isFalse();
        assertThat(rule.isUserModified()).isTrue();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        writeRuleXml(rule, baos);
@@ -916,7 +934,7 @@ public class ZenModeConfigTest extends UiServiceTestCase {
        ZenModeConfig.ZenRule fromXml = readRuleXml(bais);

        assertThat(fromXml.userModifiedFields).isEqualTo(rule.userModifiedFields);
        assertThat(fromXml.canBeUpdatedByApp()).isFalse();
        assertThat(fromXml.isUserModified()).isTrue();
    }

    @Test
+2 −1
Original line number Diff line number Diff line
@@ -475,7 +475,8 @@ public class ZenModeDiffTest extends UiServiceTestCase {
        // "Metadata" fields are never compared.
        Set<String> exemptFields = new LinkedHashSet<>(
                Set.of("userModifiedFields", "zenPolicyUserModifiedFields",
                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin"));
                        "zenDeviceEffectsUserModifiedFields", "deletionInstant", "disabledOrigin",
                        "lastActivation"));
        // Flagged fields are only compared if their flag is on.
        if (Flags.modesUi()) {
            exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete.
Loading