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

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

Distinguish implicit modes in ZenMode

Also add an icon getter for ZenMode that returns a fixed icon for all implicit modes (for possible use in lock screen / status bar).

Fixes: 360817586
Test: atest ZenModeTest
Flag: android.app.modes_ui
Change-Id: I6e0f770096c52f99b427f6789ff4f44bb168544c
parent d0615c0f
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -187,6 +187,13 @@ public class ZenModeConfig implements Parcelable {
    @Retention(RetentionPolicy.SOURCE)
    public @interface ConfigOrigin {}

    /**
     * Prefix for the ids of implicit Zen rules. Implicit rules are those created automatically
     * on behalf of apps that call {@link NotificationManager#setNotificationPolicy} or
     * {@link NotificationManager#setInterruptionFilter}.
     */
    private static final String IMPLICIT_RULE_ID_PREFIX = "implicit_"; // + pkg_name

    public static final int SOURCE_ANYONE = Policy.PRIORITY_SENDERS_ANY;
    public static final int SOURCE_CONTACT = Policy.PRIORITY_SENDERS_CONTACTS;
    public static final int SOURCE_STAR = Policy.PRIORITY_SENDERS_STARRED;
@@ -2492,6 +2499,16 @@ public class ZenModeConfig implements Parcelable {

    // ==== End built-in system conditions ====

    /** Generate the rule id for the implicit rule for the specified package. */
    public static String implicitRuleId(String forPackage) {
        return IMPLICIT_RULE_ID_PREFIX + forPackage;
    }

    /** Returns whether the rule id corresponds to an implicit rule. */
    public static boolean isImplicitRuleId(@NonNull String ruleId) {
        return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
    }

    private static int[] tryParseHourAndMinute(String value) {
        if (TextUtils.isEmpty(value)) return null;
        final int i = value.indexOf('.');
+61 −19
Original line number Diff line number Diff line
@@ -117,6 +117,21 @@ public class ZenMode implements Parcelable {
            .thenComparing(ZenMode::getType, PRIORITIZED_TYPE_COMPARATOR)
            .thenComparing(ZenMode::getName);

    public enum Kind {
        /** A "normal" mode, created by apps or users via {@code addAutomaticZenRule()}. */
        NORMAL,

        /** The special, built-in "Do Not Disturb" mode. */
        MANUAL_DND,

        /**
         * An implicit mode, automatically created and managed by the system on behalf of apps that
         * call {@code setInterruptionFilter()} or {@code setNotificationPolicy()} (with some
         * exceptions).
         */
        IMPLICIT,
    }

    public enum Status {
        ENABLED,
        ENABLED_AND_ACTIVE,
@@ -126,8 +141,8 @@ public class ZenMode implements Parcelable {

    private final String mId;
    private final AutomaticZenRule mRule;
    private final Kind mKind;
    private final Status mStatus;
    private final boolean mIsManualDnd;

    /**
     * Initializes a {@link ZenMode}, mainly based on the information from the
@@ -137,9 +152,11 @@ public class ZenMode implements Parcelable {
     * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} --
     * see {@link #computeStatus}.
     */
    public ZenMode(String id, @NonNull AutomaticZenRule rule,
    ZenMode(String id, @NonNull AutomaticZenRule rule,
            @NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
        this(id, rule, computeStatus(zenRuleExtraData), false);
        this(id, rule,
                ZenModeConfig.isImplicitRuleId(id) ? Kind.IMPLICIT : Kind.NORMAL,
                computeStatus(zenRuleExtraData));
    }

    private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) {
@@ -158,13 +175,16 @@ public class ZenMode implements Parcelable {
        }
    }

    public static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
    static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
        // Manual rule is owned by the system, so we set it here
        AutomaticZenRule manualRuleWithPkg = new AutomaticZenRule.Builder(manualRule)
                .setPackage(PACKAGE_ANDROID)
                .build();
        return new ZenMode(MANUAL_DND_MODE_ID, manualRuleWithPkg,
                isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED, true);
        return new ZenMode(
                MANUAL_DND_MODE_ID,
                manualRuleWithPkg,
                Kind.MANUAL_DND,
                isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED);
    }

    /**
@@ -183,19 +203,19 @@ public class ZenMode implements Parcelable {
                .setIconResId(iconResId)
                .setManualInvocationAllowed(true)
                .build();
        return new ZenMode(TEMP_NEW_MODE_ID, rule, Status.ENABLED, false);
        return new ZenMode(TEMP_NEW_MODE_ID, rule, Kind.NORMAL, Status.ENABLED);
    }

    private ZenMode(String id, @NonNull AutomaticZenRule rule, Status status, boolean isManualDnd) {
    private ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status) {
        mId = id;
        mRule = rule;
        mKind = kind;
        mStatus = status;
        mIsManualDnd = isManualDnd;
    }

    /** Creates a deep copy of this object. */
    public ZenMode copy() {
        return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mStatus, mIsManualDnd);
        return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mKind, mStatus);
    }

    @NonNull
@@ -264,10 +284,32 @@ public class ZenMode implements Parcelable {
        return mRule.getType() + ":" + mRule.getPackageName() + ":" + mRule.getIconResId();
    }

    /**
     * Returns the mode icon -- which can be either app-provided (via {@code addAutomaticZenRule}),
     * user-chosen (via the icon picker in Settings), the app's launcher icon for implicit rules
     * (in its monochrome variant, if available), or a default icon based on the mode type.
     */
    @NonNull
    public ListenableFuture<Drawable> getIcon(@NonNull Context context,
            @NonNull ZenIconLoader iconLoader) {
        if (mIsManualDnd) {
        if (mKind == Kind.MANUAL_DND) {
            return Futures.immediateFuture(requireNonNull(
                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
        }

        return iconLoader.getIcon(context, mRule);
    }

    /**
     * Returns an alternative mode icon. The difference with {@link #getIcon} is that it's the
     * basic DND icon not only for Manual DND, but also for <em>implicit rules</em>. As such, it's
     * suitable for places where showing the launcher icon of an app could be confusing, such as
     * the status bar or lockscreen.
     */
    @NonNull
    public ListenableFuture<Drawable> getLockscreenIcon(@NonNull Context context,
            @NonNull ZenIconLoader iconLoader) {
        if (mKind == Kind.MANUAL_DND || mKind == Kind.IMPLICIT) {
            return Futures.immediateFuture(requireNonNull(
                    context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
        }
@@ -373,7 +415,7 @@ public class ZenMode implements Parcelable {
    }

    public boolean isManualDnd() {
        return mIsManualDnd;
        return mKind == Kind.MANUAL_DND;
    }

    /**
@@ -404,18 +446,18 @@ public class ZenMode implements Parcelable {
        return obj instanceof ZenMode other
                && mId.equals(other.mId)
                && mRule.equals(other.mRule)
                && mStatus.equals(other.mStatus)
                && mIsManualDnd == other.mIsManualDnd;
                && mKind.equals(other.mKind)
                && mStatus.equals(other.mStatus);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mId, mRule, mStatus, mIsManualDnd);
        return Objects.hash(mId, mRule, mKind, mStatus);
    }

    @Override
    public String toString() {
        return mId + " (" + mStatus + ") -> " + mRule;
        return mId + " (" + mKind + ", " + mStatus + ") -> " + mRule;
    }

    @Override
@@ -427,8 +469,8 @@ public class ZenMode implements Parcelable {
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString(mId);
        dest.writeParcelable(mRule, 0);
        dest.writeString(mKind.name());
        dest.writeString(mStatus.name());
        dest.writeBoolean(mIsManualDnd);
    }

    public static final Creator<ZenMode> CREATOR = new Creator<ZenMode>() {
@@ -438,8 +480,8 @@ public class ZenMode implements Parcelable {
                    in.readString(),
                    checkNotNull(in.readParcelable(AutomaticZenRule.class.getClassLoader(),
                            AutomaticZenRule.class)),
                    Status.valueOf(in.readString()),
                    in.readBoolean());
                    Kind.valueOf(in.readString()),
                    Status.valueOf(in.readString()));
        }

        @Override
+90 −0
Original line number Diff line number Diff line
@@ -30,7 +30,14 @@ import static android.service.notification.SystemZenRules.PACKAGE_ANDROID;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;

import android.app.AutomaticZenRule;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Parcel;
import android.service.notification.Condition;
@@ -40,9 +47,12 @@ import android.service.notification.ZenPolicy;

import com.android.internal.R;

import com.google.common.util.concurrent.ListenableFuture;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;

import java.util.ArrayList;
import java.util.List;
@@ -60,6 +70,13 @@ public class ZenModeTest {
                    .setZenPolicy(ZEN_POLICY)
                    .build();

    private static final String IMPLICIT_RULE_ID = ZenModeConfig.implicitRuleId("some.package");
    private static final AutomaticZenRule IMPLICIT_ZEN_RULE =
            new AutomaticZenRule.Builder("Implicit", Uri.parse("implicit/some.package"))
                    .setPackage("some.package")
                    .setType(TYPE_OTHER)
                    .build();

    @Test
    public void testBasicMethods() {
        ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true));
@@ -265,6 +282,79 @@ public class ZenModeTest {

        assertUnparceledIsEqualToOriginal("custom_manual",
                ZenMode.newCustomManual("New mode", R.drawable.ic_zen_mode_type_immersive));

        assertUnparceledIsEqualToOriginal("implicit",
                new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
                        zenConfigRuleFor(IMPLICIT_ZEN_RULE, false)));
    }

    @Test
    public void getIcon_normalMode_loadsIconNormally() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));

        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
                iconLoader);

        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
    }

    @Test
    public void getIcon_manualDnd_returnsFixedIcon() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);

        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getIcon(
                RuntimeEnvironment.getApplication(), iconLoader);

        assertThat(future.isDone()).isTrue();
        verify(iconLoader, never()).getIcon(any(), any());
    }

    @Test
    public void getIcon_implicitMode_loadsIconNormally() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));

        ListenableFuture<Drawable> unused = mode.getIcon(RuntimeEnvironment.getApplication(),
                iconLoader);

        verify(iconLoader).getIcon(any(), eq(IMPLICIT_ZEN_RULE));
    }

    @Test
    public void getLockscreenIcon_normalMode_loadsIconNormally() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
        ZenMode mode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, false));

        ListenableFuture<Drawable> unused = mode.getLockscreenIcon(
                RuntimeEnvironment.getApplication(), iconLoader);

        verify(iconLoader).getIcon(any(), eq(ZEN_RULE));
    }

    @Test
    public void getLockscreenIcon_manualDnd_returnsFixedIcon() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);

        ListenableFuture<Drawable> future = TestModeBuilder.MANUAL_DND_INACTIVE.getLockscreenIcon(
                RuntimeEnvironment.getApplication(), iconLoader);

        assertThat(future.isDone()).isTrue();
        verify(iconLoader, never()).getIcon(any(), any());
    }

    @Test
    public void getLockscreenIcon_implicitMode_returnsFixedIcon() {
        ZenIconLoader iconLoader = mock(ZenIconLoader.class);
        ZenMode mode = new ZenMode(IMPLICIT_RULE_ID, IMPLICIT_ZEN_RULE,
                zenConfigRuleFor(IMPLICIT_ZEN_RULE, false));

        ListenableFuture<Drawable> future = mode.getLockscreenIcon(
                RuntimeEnvironment.getApplication(), iconLoader);

        assertThat(future.isDone()).isTrue();
        verify(iconLoader, never()).getIcon(any(), any());
    }

    private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) {
+1 −1
Original line number Diff line number Diff line
@@ -5675,7 +5675,7 @@ public class NotificationManagerService extends SystemService {
            // a "normal" rule, it must provide a CP/ConfigActivity too.
            if (android.app.Flags.modesApi()) {
                boolean isImplicitRuleUpdateFromSystem = updateId != null
                        && ZenModeHelper.isImplicitRuleId(updateId)
                        && ZenModeConfig.isImplicitRuleId(updateId)
                        && isCallerSystemOrSystemUi();
                if (!isImplicitRuleUpdateFromSystem
                        && rule.getOwner() == null
+1 −10
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP;
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
import static android.service.notification.ZenModeConfig.implicitRuleId;

import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -155,8 +156,6 @@ public class ZenModeHelper {
    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

    private static final int MAX_ICON_RESOURCE_NAME_LENGTH = 1000;

    /**
@@ -783,14 +782,6 @@ public class ZenModeHelper {
        return rule;
    }

    private static String implicitRuleId(String forPackage) {
        return IMPLICIT_RULE_ID_PREFIX + forPackage;
    }

    static boolean isImplicitRuleId(@NonNull String ruleId) {
        return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX);
    }

    boolean removeAutomaticZenRule(String id, @ConfigOrigin int origin, String reason,
            int callingUid) {
        checkManageRuleOrigin("removeAutomaticZenRule", origin);