1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.notification.modes;
18 
19 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
20 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
21 
22 import static java.util.Objects.requireNonNull;
23 
24 import android.annotation.SuppressLint;
25 import android.app.AutomaticZenRule;
26 import android.app.NotificationManager;
27 import android.content.Context;
28 import android.graphics.drawable.Drawable;
29 import android.service.notification.ZenDeviceEffects;
30 import android.service.notification.ZenPolicy;
31 import android.util.Log;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 
36 import com.android.settings.R;
37 
38 import com.google.common.util.concurrent.Futures;
39 import com.google.common.util.concurrent.ListenableFuture;
40 
41 import java.util.Objects;
42 
43 /**
44  * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way.
45  *
46  * <p>It also adapts other rule features that we don't want to expose in the UI, such as
47  * interruption filters other than {@code PRIORITY}, rules without specific icons, etc.
48  */
49 class ZenMode {
50 
51     private static final String TAG = "ZenMode";
52 
53     /**
54      * Additional value for the {@code @ZenPolicy.ChannelType} enumeration that indicates that all
55      * channels can bypass DND when this policy is active.
56      *
57      * <p>This value shouldn't be used on "real" ZenPolicy objects sent to or returned from
58      * {@link android.app.NotificationManager}; it's a way of representing rules with interruption
59      * filter = {@link NotificationManager#INTERRUPTION_FILTER_ALL} in the UI.
60      */
61     public static final int CHANNEL_POLICY_ALL = -1;
62 
63     static final String MANUAL_DND_MODE_ID = "manual_dnd";
64 
65     @SuppressLint("WrongConstant")
66     private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALL =
67             new ZenPolicy.Builder()
68                     .allowChannels(CHANNEL_POLICY_ALL)
69                     .allowAllSounds()
70                     .showAllVisualEffects()
71                     .build();
72 
73     // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
74     private static final ZenPolicy POLICY_INTERRUPTION_FILTER_ALARMS =
75             new ZenPolicy.Builder()
76                     .disallowAllSounds()
77                     .allowAlarms(true)
78                     .allowMedia(true)
79                     .allowPriorityChannels(false)
80                     .build();
81 
82     // Must match com.android.server.notification.ZenModeHelper#applyCustomPolicy.
83     private static final ZenPolicy POLICY_INTERRUPTION_FILTER_NONE =
84             new ZenPolicy.Builder()
85                     .disallowAllSounds()
86                     .hideAllVisualEffects()
87                     .allowPriorityChannels(false)
88                     .build();
89 
90     private final String mId;
91     private AutomaticZenRule mRule;
92     private final boolean mIsActive;
93     private final boolean mIsManualDnd;
94 
ZenMode(String id, AutomaticZenRule rule, boolean isActive)95     ZenMode(String id, AutomaticZenRule rule, boolean isActive) {
96         this(id, rule, isActive, false);
97     }
98 
ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd)99     private ZenMode(String id, AutomaticZenRule rule, boolean isActive, boolean isManualDnd) {
100         mId = id;
101         mRule = rule;
102         mIsActive = isActive;
103         mIsManualDnd = isManualDnd;
104     }
105 
manualDndMode(AutomaticZenRule manualRule, boolean isActive)106     static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) {
107         return new ZenMode(MANUAL_DND_MODE_ID, manualRule, isActive, true);
108     }
109 
110     @NonNull
getId()111     public String getId() {
112         return mId;
113     }
114 
115     @NonNull
getRule()116     public AutomaticZenRule getRule() {
117         return mRule;
118     }
119 
120     @NonNull
getIcon(@onNull Context context, @NonNull IconLoader iconLoader)121     public ListenableFuture<Drawable> getIcon(@NonNull Context context,
122             @NonNull IconLoader iconLoader) {
123         if (mIsManualDnd) {
124             return Futures.immediateFuture(requireNonNull(
125                     context.getDrawable(R.drawable.ic_do_not_disturb_on_24dp)));
126         }
127 
128         return iconLoader.getIcon(context, mRule);
129     }
130 
131     @NonNull
getPolicy()132     public ZenPolicy getPolicy() {
133         switch (mRule.getInterruptionFilter()) {
134             case INTERRUPTION_FILTER_PRIORITY:
135                 return requireNonNull(mRule.getZenPolicy());
136 
137             case NotificationManager.INTERRUPTION_FILTER_ALL:
138                 return POLICY_INTERRUPTION_FILTER_ALL;
139 
140             case NotificationManager.INTERRUPTION_FILTER_ALARMS:
141                 return POLICY_INTERRUPTION_FILTER_ALARMS;
142 
143             case NotificationManager.INTERRUPTION_FILTER_NONE:
144                 return POLICY_INTERRUPTION_FILTER_NONE;
145 
146             case NotificationManager.INTERRUPTION_FILTER_UNKNOWN:
147             default:
148                 Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter "
149                         + mRule.getInterruptionFilter());
150                 return requireNonNull(mRule.getZenPolicy());
151         }
152     }
153 
154     /**
155      * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the
156      * supplied policy. In some cases this involves conversions, so that the following call
157      * to {@link #getPolicy} might return a different policy from the one supplied here.
158      */
159     @SuppressLint("WrongConstant")
setPolicy(@onNull ZenPolicy policy)160     public void setPolicy(@NonNull ZenPolicy policy) {
161         ZenPolicy currentPolicy = getPolicy();
162         if (currentPolicy.equals(policy)) {
163             return;
164         }
165 
166         // A policy with CHANNEL_POLICY_ALL is only a UI representation of the
167         // INTERRUPTION_FILTER_ALL filter. Thus, switching to or away to this value only updates
168         // the filter, discarding the rest of the supplied policy.
169         if (policy.getAllowedChannels() == CHANNEL_POLICY_ALL
170                 && currentPolicy.getAllowedChannels() != CHANNEL_POLICY_ALL) {
171             if (mIsManualDnd) {
172                 throw new IllegalArgumentException("Manual DND cannot have CHANNEL_POLICY_ALL");
173             }
174             mRule.setInterruptionFilter(INTERRUPTION_FILTER_ALL);
175             // Preserve the existing policy, e.g. if the user goes PRIORITY -> ALL -> PRIORITY that
176             // shouldn't discard all other policy customizations. The existing policy will be a
177             // synthetic one if the rule originally had filter NONE or ALARMS_ONLY and that's fine.
178             if (mRule.getZenPolicy() == null) {
179                 mRule.setZenPolicy(currentPolicy);
180             }
181             return;
182         } else if (policy.getAllowedChannels() != CHANNEL_POLICY_ALL
183                 && currentPolicy.getAllowedChannels() == CHANNEL_POLICY_ALL) {
184             mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
185             // Go back to whatever policy the rule had before, unless the rule never had one, in
186             // which case we use the supplied policy (which we know has a valid allowedChannels).
187             if (mRule.getZenPolicy() == null) {
188                 mRule.setZenPolicy(policy);
189             }
190             return;
191         }
192 
193         // If policy is customized from any of the "special" ones, make the rule PRIORITY.
194         if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) {
195             mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY);
196         }
197         mRule.setZenPolicy(policy);
198     }
199 
200     @NonNull
getDeviceEffects()201     public ZenDeviceEffects getDeviceEffects() {
202         return mRule.getDeviceEffects() != null
203                 ? mRule.getDeviceEffects()
204                 : new ZenDeviceEffects.Builder().build();
205     }
206 
canEditName()207     public boolean canEditName() {
208         return !isManualDnd();
209     }
210 
canEditIcon()211     public boolean canEditIcon() {
212         return !isManualDnd();
213     }
214 
canBeDeleted()215     public boolean canBeDeleted() {
216         return !mIsManualDnd;
217     }
218 
isManualDnd()219     public boolean isManualDnd() {
220         return mIsManualDnd;
221     }
222 
isActive()223     public boolean isActive() {
224         return mIsActive;
225     }
226 
227     @Override
equals(@ullable Object obj)228     public boolean equals(@Nullable Object obj) {
229         return obj instanceof ZenMode other
230                 && mId.equals(other.mId)
231                 && mRule.equals(other.mRule)
232                 && mIsActive == other.mIsActive;
233     }
234 
235     @Override
hashCode()236     public int hashCode() {
237         return Objects.hash(mId, mRule, mIsActive);
238     }
239 
240     @Override
toString()241     public String toString() {
242         return mId + "(" + (mIsActive ? "active" : "inactive") + ") -> " + mRule;
243     }
244 }
245