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