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

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

Introduce ZenModesBackend and ZenMode

Test: atest ZenModesBackendTest ZenModeTest
Bug: 327419222
Change-Id: Ic2871a6124b2df4b77275b54a940f7b47666991c
parent c365d864
Loading
Loading
Loading
Loading
+209 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification.modes;

import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;

import static java.util.Objects.requireNonNull;

import android.app.AutomaticZenRule;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.service.notification.SystemZenRules;
import android.service.notification.ZenPolicy;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;

import com.android.settings.R;

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

import java.util.Objects;

/**
 * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
 *
 * <p>It also adapts other rule features that we don't want to expose in the UI, such as
 * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
 */
class ZenMode {

    private static final String TAG = "ZenMode";

    static final String MANUAL_DND_MODE_ID = "manual_dnd";

    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALL =
            // TODO: b/331267485 - Support "allow all channels"!
            new ZenPolicy.Builder().allowAllSounds().showAllVisualEffects().build();

    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
            new ZenPolicy.Builder()
                    .disallowAllSounds()
                    .allowAlarms(true)
                    .allowMedia(true)
                    .allowPriorityChannels(false)
                    .build();

    // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
    private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
            new ZenPolicy.Builder()
                    .disallowAllSounds()
                    .hideAllVisualEffects()
                    .allowPriorityChannels(false)
                    .build();

    private final String mId;
    private final AutomaticZenRule mRule;
    private final boolean mIsManualDnd;

    ZenMode(String id, AutomaticZenRule rule) {
        this(id, rule, false);
    }

    private ZenMode(String id, AutomaticZenRule rule, boolean isManualDnd) {
        mId = id;
        mRule = rule;
        mIsManualDnd = isManualDnd;
    }

    static ZenMode manualDndMode(AutomaticZenRule dndPolicyAsRule) {
        return new ZenMode(MANUAL_DND_MODE_ID, dndPolicyAsRule, true);
    }

    @NonNull
    public String getId() {
        return mId;
    }

    @NonNull
    public AutomaticZenRule getRule() {
        return mRule;
    }

    @NonNull
    public ListenableFuture<Drawable> getIcon(@NonNull Context context) {
        // TODO: b/333528586 - Load the icons asynchronously, and cache them
        if (mIsManualDnd) {
            return Futures.immediateFuture(
                    requireNonNull(context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
        }

        int iconResId = mRule.getIconResId();
        Drawable customIcon = null;
        if (iconResId != 0) {
            if (SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName())) {
                customIcon = context.getDrawable(mRule.getIconResId());
            } else {
                try {
                    Context appContext = context.createPackageContext(mRule.getPackageName(), 0);
                    customIcon = AppCompatResources.getDrawable(appContext, mRule.getIconResId());
                } catch (PackageManager.NameNotFoundException e) {
                    Log.wtf(TAG,
                            "Package " + mRule.getPackageName() + " used in rule " + mId
                                    + " not found?", e);
                    // Continue down to use a default icon.
                }
            }
        }
        if (customIcon != null) {
            return Futures.immediateFuture(customIcon);
        }

        // Derive a default icon from the rule type.
        // TODO: b/333528437 - Use correct icons
        int iconResIdFromType = switch (mRule.getType()) {
            case AutomaticZenRule.TYPE_UNKNOWN -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_OTHER -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_SCHEDULE_TIME -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_SCHEDULE_CALENDAR -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_BEDTIME -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_DRIVING -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_IMMERSIVE -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_THEATER -> R.drawable.ic_do_not_disturb_on_24dp;
            case AutomaticZenRule.TYPE_MANAGED -> R.drawable.ic_do_not_disturb_on_24dp;
            default -> R.drawable.ic_do_not_disturb_on_24dp;
        };
        return Futures.immediateFuture(requireNonNull(context.getDrawable(iconResIdFromType)));
    }

    @NonNull
    public ZenPolicy getPolicy() {
        switch (mRule.getInterruptionFilter()) {
            case INTERRUPTION_FILTER_PRIORITY:
                return requireNonNull(mRule.getZenPolicy());

            case NotificationManager.INTERRUPTION_FILTER_ALL:
                return POLICY_INTERRUPTION_FILTER_ALL;

            case NotificationManager.INTERRUPTION_FILTER_ALARMS:
                return POLICY_INTERRUPTION_FILTER_ALARMS;

            case NotificationManager.INTERRUPTION_FILTER_NONE:
                return POLICY_INTERRUPTION_FILTER_NONE;

            case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
            default:
                Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
                        + mRule.getInterruptionFilter());
                return requireNonNull(mRule.getZenPolicy());
        }
    }

    public void setZenPolicy(@NonNull ZenPolicy policy) {
        // TODO: b/331267485 - A policy with apps=ALL should be mapped to INTERRUPTION_FILTER_ALL.
        if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
            ZenPolicy currentPolicy = getPolicy();
            if (!currentPolicy.equals(policy)) {
                // If policy is customized from any of the "special" ones, make the rule PRIORITY.
                mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
            }
        }
        mRule.setZenPolicy(policy);
    }

    public boolean canBeDeleted() {
        return !mIsManualDnd;
    }

    public boolean isManualDnd() {
        return mIsManualDnd;
    }

    @Override
    public boolean equals(@Nullable Object obj) {
        return obj instanceof ZenMode other
                && mId.equals(other.mId)
                && mRule.equals(other.mRule);
    }

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

    @Override
    public String toString() {
        return mId + " -> " + mRule;
    }
}
+161 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification.modes;

import static java.util.Objects.requireNonNull;

import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.AutomaticZenRule;
import android.app.NotificationManager;
import android.content.Context;
import android.net.Uri;
import android.provider.Settings;
import android.service.notification.Condition;
import android.service.notification.ZenAdapters;
import android.service.notification.ZenModeConfig;

import com.android.settings.R;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Class used for Settings-NMS interactions related to Mode management.
 *
 * <p>This class converts {@link AutomaticZenRule} instances, as well as the manual zen mode,
 * into the unified {@link ZenMode} format.
 */
class ZenModesBackend {

    private static final String TAG = "ZenModeBackend";

    @Nullable // Until first usage
    private static ZenModesBackend sInstance;

    private final NotificationManager mNotificationManager;

    private final Context mContext;

    static ZenModesBackend getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new ZenModesBackend(context.getApplicationContext());
        }
        return sInstance;
    }

    ZenModesBackend(Context context) {
        mContext = context;
        mNotificationManager = context.getSystemService(NotificationManager.class);
    }

    List<ZenMode> getModes() {
        ArrayList<ZenMode> modes = new ArrayList<>();
        modes.add(getManualDndMode());

        Map<String, AutomaticZenRule> zenRules = mNotificationManager.getAutomaticZenRules();
        for (Map.Entry<String, AutomaticZenRule> zenRuleEntry : zenRules.entrySet()) {
            modes.add(new ZenMode(zenRuleEntry.getKey(), zenRuleEntry.getValue()));
        }

        // TODO: b/331429435 - Sort modes.
        return modes;
    }

    @Nullable
    ZenMode getMode(String id) {
        if (ZenMode.MANUAL_DND_MODE_ID.equals(id)) {
            return getManualDndMode();
        } else {
            AutomaticZenRule rule = mNotificationManager.getAutomaticZenRule(id);
            return rule != null ? new ZenMode(id, rule) : null;
        }
    }

    private ZenMode getManualDndMode() {
        // TODO: b/333530553 - Read ZenDeviceEffects of manual DND.
        // TODO: b/333682392 - Replace with final strings for name & trigger description
        AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
                mContext.getString(R.string.zen_mode_settings_title), Uri.EMPTY)
                .setType(AutomaticZenRule.TYPE_OTHER)
                .setZenPolicy(ZenAdapters.notificationPolicyToZenPolicy(
                        mNotificationManager.getNotificationPolicy()))
                .setDeviceEffects(null)
                .setTriggerDescription(mContext.getString(R.string.zen_mode_settings_summary))
                .setManualInvocationAllowed(true)
                .setConfigurationActivity(null) // No further settings
                .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
                .build();

        return ZenMode.manualDndMode(manualDndRule);
    }

    void updateMode(ZenMode mode) {
        if (mode.isManualDnd()) {
            NotificationManager.Policy dndPolicy =
                    new ZenModeConfig().toNotificationPolicy(requireNonNull(mode.getPolicy()));
            mNotificationManager.setNotificationPolicy(dndPolicy, /* fromUser= */ true);
            // TODO: b/333530553 - Update ZenDeviceEffects of the manual DND too.
        } else {
            mNotificationManager.updateAutomaticZenRule(mode.getId(), mode.getRule(),
                    /* fromUser= */ true);
        }
    }

    void activateMode(ZenMode mode, @Nullable Duration forDuration) {
        if (mode.isManualDnd()) {
            Uri durationConditionId = null;
            if (forDuration != null) {
                durationConditionId = ZenModeConfig.toTimeCondition(mContext,
                        (int) forDuration.toMinutes(), ActivityManager.getCurrentUser(), true).id;
            }
            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                    durationConditionId, TAG, /* fromUser= */ true);

        } else {
            if (forDuration != null) {
                throw new IllegalArgumentException(
                        "Only the manual DND mode can be activated for a specific duration");
            }
            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_TRUE,
                            Condition.SOURCE_USER_ACTION));
        }
    }

    void deactivateMode(ZenMode mode) {
        if (mode.isManualDnd()) {
            // TODO: b/326061620 - This shouldn't snooze any rules that are active.
            mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
                    /* fromUser= */ true);
        } else {
            // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
            mNotificationManager.setAutomaticZenRuleState(mode.getId(),
                    new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
                            Condition.SOURCE_USER_ACTION));
        }
    }

    void removeMode(ZenMode mode) {
        if (!mode.canBeDeleted()) {
            throw new IllegalArgumentException("Mode " + mode + " cannot be deleted!");
        }
        mNotificationManager.removeAutomaticZenRule(mode.getId(), /* fromUser= */ true);
    }
}
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settings.notification.modes;

import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;

import static com.google.common.truth.Truth.assertThat;

import android.app.AutomaticZenRule;
import android.net.Uri;
import android.service.notification.ZenPolicy;

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

@RunWith(RobolectricTestRunner.class)
public class ZenModeTest {

    private static final ZenPolicy ZEN_POLICY = new ZenPolicy.Builder().allowAllSounds().build();

    private static final AutomaticZenRule ZEN_RULE =
            new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
                    .setType(AutomaticZenRule.TYPE_DRIVING)
                    .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                    .setZenPolicy(ZEN_POLICY)
                    .build();

    @Test
    public void testBasicMethods() {
        ZenMode zenMode = new ZenMode("id", ZEN_RULE);

        assertThat(zenMode.getId()).isEqualTo("id");
        assertThat(zenMode.getRule()).isEqualTo(ZEN_RULE);
        assertThat(zenMode.isManualDnd()).isFalse();
        assertThat(zenMode.canBeDeleted()).isTrue();
    }

    @Test
    public void getZenPolicy_interruptionFilterPriority_returnsZenPolicy() {
        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
                .setZenPolicy(ZEN_POLICY)
                .build());

        assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY);
    }

    @Test
    public void getZenPolicy_interruptionFilterAll_returnsPolicyAllowingAll() {
        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                .setInterruptionFilter(INTERRUPTION_FILTER_ALL)
                .setZenPolicy(ZEN_POLICY) // should be ignored
                .build());

        assertThat(zenMode.getPolicy()).isEqualTo(
                new ZenPolicy.Builder().allowAllSounds().showAllVisualEffects().build());
    }

    @Test
    public void getZenPolicy_interruptionFilterAlarms_returnsPolicyAllowingAlarms() {
        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS)
                .setZenPolicy(ZEN_POLICY) // should be ignored
                .build());

        assertThat(zenMode.getPolicy()).isEqualTo(
                new ZenPolicy.Builder()
                        .disallowAllSounds()
                        .allowAlarms(true)
                        .allowMedia(true)
                        .allowPriorityChannels(false)
                        .build());
    }

    @Test
    public void getZenPolicy_interruptionFilterNone_returnsPolicyAllowingNothing() {
        ZenMode zenMode = new ZenMode("id", new AutomaticZenRule.Builder("Rule", Uri.EMPTY)
                .setInterruptionFilter(INTERRUPTION_FILTER_NONE)
                .setZenPolicy(ZEN_POLICY) // should be ignored
                .build());

        assertThat(zenMode.getPolicy()).isEqualTo(
                new ZenPolicy.Builder()
                        .disallowAllSounds()
                        .hideAllVisualEffects()
                        .allowPriorityChannels(false)
                        .build());
    }
}
+243 −0

File added.

Preview size limit exceeded, changes collapsed.