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.settingslib.notification.modes; 18 19 import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; 20 import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; 21 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; 22 import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; 23 import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; 24 import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; 25 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent; 26 import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime; 27 import static android.service.notification.ZenModeConfig.tryParseEventConditionId; 28 import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId; 29 30 import static com.google.common.base.Preconditions.checkNotNull; 31 import static com.google.common.base.Preconditions.checkState; 32 33 import static java.util.Objects.requireNonNull; 34 35 import android.annotation.SuppressLint; 36 import android.app.AutomaticZenRule; 37 import android.app.NotificationManager; 38 import android.content.ComponentName; 39 import android.content.Context; 40 import android.net.Uri; 41 import android.os.Parcel; 42 import android.os.Parcelable; 43 import android.service.notification.SystemZenRules; 44 import android.service.notification.ZenDeviceEffects; 45 import android.service.notification.ZenModeConfig; 46 import android.service.notification.ZenPolicy; 47 import android.util.Log; 48 49 import androidx.annotation.DrawableRes; 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 53 import com.google.common.base.Strings; 54 import com.google.common.collect.ImmutableList; 55 56 import java.util.Comparator; 57 import java.util.Objects; 58 59 /** 60 * Represents either an {@link AutomaticZenRule} or the manual DND rule in a unified way. 61 * 62 * <p>It also adapts other rule features that we don't want to expose in the UI, such as 63 * interruption filters other than {@code PRIORITY}, rules without specific icons, etc. 64 */ 65 public class ZenMode implements Parcelable { 66 67 private static final String TAG = "ZenMode"; 68 69 static final String MANUAL_DND_MODE_ID = ZenModeConfig.MANUAL_RULE_ID; 70 static final String TEMP_NEW_MODE_ID = "temp_new_mode"; 71 72 private static final Comparator<Integer> PRIORITIZED_TYPE_COMPARATOR = new Comparator<>() { 73 74 private static final ImmutableList</* @AutomaticZenRule.Type */ Integer> 75 PRIORITIZED_TYPES = ImmutableList.of( 76 AutomaticZenRule.TYPE_BEDTIME, 77 AutomaticZenRule.TYPE_DRIVING); 78 79 @Override 80 public int compare(Integer first, Integer second) { 81 if (PRIORITIZED_TYPES.contains(first) && PRIORITIZED_TYPES.contains(second)) { 82 return PRIORITIZED_TYPES.indexOf(first) - PRIORITIZED_TYPES.indexOf(second); 83 } else if (PRIORITIZED_TYPES.contains(first)) { 84 return -1; 85 } else if (PRIORITIZED_TYPES.contains(second)) { 86 return 1; 87 } else { 88 return 0; 89 } 90 } 91 }; 92 93 // Manual DND first, Bedtime/Driving, then alphabetically. 94 public static final Comparator<ZenMode> PRIORITIZING_COMPARATOR = Comparator 95 .comparing(ZenMode::isManualDnd).reversed() 96 .thenComparing(ZenMode::getType, PRIORITIZED_TYPE_COMPARATOR) 97 .thenComparing(ZenMode::getName); 98 99 public enum Kind { 100 /** A "normal" mode, created by apps or users via {@code addAutomaticZenRule()}. */ 101 NORMAL, 102 103 /** The special, built-in "Do Not Disturb" mode. */ 104 MANUAL_DND, 105 106 /** 107 * An implicit mode, automatically created and managed by the system on behalf of apps that 108 * call {@code setInterruptionFilter()} or {@code setNotificationPolicy()} (with some 109 * exceptions). 110 */ 111 IMPLICIT, 112 } 113 114 public enum Status { 115 ENABLED, 116 ENABLED_AND_ACTIVE, 117 DISABLED_BY_USER, 118 DISABLED_BY_OTHER 119 } 120 121 /** 122 * Information about the owner of a {@link ZenMode}. {@link #packageName()} is 123 * {@link SystemZenRules#PACKAGE_ANDROID} if the mode is system-owned; it may also be 124 * {@code null}, but only as an artifact of very old modes. 125 */ Owner(@ullable String packageName, @Nullable ComponentName configurationActivity, @Nullable ComponentName conditionProvider)126 public record Owner(@Nullable String packageName, @Nullable ComponentName configurationActivity, 127 @Nullable ComponentName conditionProvider) { } 128 129 private final String mId; 130 private final AutomaticZenRule mRule; 131 private final Kind mKind; 132 private final Status mStatus; 133 134 /** 135 * Initializes a {@link ZenMode}, mainly based on the information from the 136 * {@link AutomaticZenRule}. 137 * 138 * <p>Some pieces which are not part of the public API (such as whether the mode is currently 139 * active, or the reason it was disabled) are read from the {@link ZenModeConfig.ZenRule} -- 140 * see {@link #computeStatus}. 141 */ ZenMode(String id, @NonNull AutomaticZenRule rule, @NonNull ZenModeConfig.ZenRule zenRuleExtraData)142 ZenMode(String id, @NonNull AutomaticZenRule rule, 143 @NonNull ZenModeConfig.ZenRule zenRuleExtraData) { 144 this(id, rule, 145 ZenModeConfig.isImplicitRuleId(id) ? Kind.IMPLICIT : Kind.NORMAL, 146 computeStatus(zenRuleExtraData)); 147 } 148 computeStatus(@onNull ZenModeConfig.ZenRule zenRuleExtraData)149 private static Status computeStatus(@NonNull ZenModeConfig.ZenRule zenRuleExtraData) { 150 if (zenRuleExtraData.enabled) { 151 if (zenRuleExtraData.isActive()) { 152 return Status.ENABLED_AND_ACTIVE; 153 } else { 154 return Status.ENABLED; 155 } 156 } else { 157 if (zenRuleExtraData.disabledOrigin == ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI) { 158 return Status.DISABLED_BY_USER; 159 } else { 160 return Status.DISABLED_BY_OTHER; // by APP, SYSTEM, UNKNOWN. 161 } 162 } 163 } 164 manualDndMode(AutomaticZenRule manualRule, boolean isActive)165 static ZenMode manualDndMode(AutomaticZenRule manualRule, boolean isActive) { 166 return new ZenMode( 167 MANUAL_DND_MODE_ID, 168 manualRule, 169 Kind.MANUAL_DND, 170 isActive ? Status.ENABLED_AND_ACTIVE : Status.ENABLED); 171 } 172 173 /** 174 * Returns a new {@link ZenMode} instance that can represent a custom_manual mode that is in the 175 * process of being created (and not yet saved). 176 * 177 * @param name mode name 178 * @param iconResId resource id of the chosen icon, {code 0} if none. 179 */ newCustomManual(String name, @DrawableRes int iconResId)180 public static ZenMode newCustomManual(String name, @DrawableRes int iconResId) { 181 AutomaticZenRule rule = new AutomaticZenRule.Builder(name, 182 ZenModeConfig.toCustomManualConditionId()) 183 .setPackage(ZenModeConfig.getCustomManualConditionProvider().getPackageName()) 184 .setType(AutomaticZenRule.TYPE_OTHER) 185 .setOwner(ZenModeConfig.getCustomManualConditionProvider()) 186 .setIconResId(iconResId) 187 .setManualInvocationAllowed(true) 188 .build(); 189 return new ZenMode(TEMP_NEW_MODE_ID, rule, Kind.NORMAL, Status.ENABLED); 190 } 191 ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status)192 private ZenMode(String id, @NonNull AutomaticZenRule rule, Kind kind, Status status) { 193 mId = id; 194 mRule = rule; 195 mKind = kind; 196 mStatus = status; 197 } 198 199 /** Creates a deep copy of this object. */ copy()200 public ZenMode copy() { 201 return new ZenMode(mId, new AutomaticZenRule.Builder(mRule).build(), mKind, mStatus); 202 } 203 204 @NonNull getId()205 public String getId() { 206 return mId; 207 } 208 209 @NonNull getRule()210 AutomaticZenRule getRule() { 211 return mRule; 212 } 213 214 @NonNull getName()215 public String getName() { 216 return Strings.nullToEmpty(mRule.getName()); 217 } 218 setName(@onNull String name)219 public void setName(@NonNull String name) { 220 mRule.setName(name); 221 } 222 223 @NonNull getKind()224 public Kind getKind() { 225 return mKind; 226 } 227 228 @NonNull getStatus()229 public Status getStatus() { 230 return mStatus; 231 } 232 233 @NonNull getOwner()234 public Owner getOwner() { 235 return new Owner(mRule.getPackageName(), mRule.getConfigurationActivity(), 236 mRule.getOwner()); 237 } 238 239 @Nullable getOwnerPackage()240 public String getOwnerPackage() { 241 return getOwner().packageName(); 242 } 243 244 @AutomaticZenRule.Type getType()245 public int getType() { 246 return mRule.getType(); 247 } 248 249 /** Returns the trigger description of the mode. */ 250 @Nullable getTriggerDescription()251 public String getTriggerDescription() { 252 return mRule.getTriggerDescription(); 253 } 254 255 /** 256 * Returns the {@link ZenIcon.Key} corresponding to the icon resource for this mode. This can be 257 * either app-provided (via {@link AutomaticZenRule#setIconResId}, user-chosen (via the icon 258 * picker in Settings), or a default icon based on the mode {@link Kind} and {@link #getType}. 259 */ 260 @NonNull getIconKey()261 public ZenIcon.Key getIconKey() { 262 if (isManualDnd()) { 263 return ZenIconKeys.MANUAL_DND; 264 } 265 if (mRule.getIconResId() != 0) { 266 if (isSystemOwned()) { 267 // System-owned rules can only have system icons. 268 return ZenIcon.Key.forSystemResource(mRule.getIconResId()); 269 } else { 270 // Technically, the icon of an app-provided rule could be a system icon if the 271 // user chose one with the picker. However, we cannot know for sure. 272 return new ZenIcon.Key(mRule.getPackageName(), mRule.getIconResId()); 273 } 274 } else { 275 // Using a default icon (which is always a system icon). 276 if (mKind == Kind.IMPLICIT) { 277 return ZenIconKeys.IMPLICIT_MODE_DEFAULT; 278 } else { 279 return ZenIconKeys.forType(getType()); 280 } 281 } 282 } 283 284 /** 285 * Returns the resource id of the icon for this mode. Note that this is the <em>stored</em> 286 * resource id, and thus can be different from the value in {@link #getIconKey()} -- in 287 * particular, for modes without a custom icon set, this method returns {@code 0} whereas 288 * {@link #getIconKey()} will return a default icon based on other mode properties. 289 * 290 * <p>Most callers are interested in {@link #getIconKey()}, unless they are editing the icon. 291 */ getIconResId()292 public int getIconResId() { 293 return mRule.getIconResId(); 294 } 295 296 /** 297 * Sets the resource id of the icon for this mode. 298 * @see #getIconResId() 299 */ setIconResId(@rawableRes int iconResId)300 public void setIconResId(@DrawableRes int iconResId) { 301 mRule.setIconResId(iconResId); 302 } 303 304 /** Returns the interruption filter of the mode. */ 305 @NotificationManager.InterruptionFilter getInterruptionFilter()306 public int getInterruptionFilter() { 307 return mRule.getInterruptionFilter(); 308 } 309 310 /** 311 * Sets the interruption filter of the mode. This is valid for {@link AutomaticZenRule}-backed 312 * modes (and not manual DND). 313 */ setInterruptionFilter(@otificationManager.InterruptionFilter int filter)314 public void setInterruptionFilter(@NotificationManager.InterruptionFilter int filter) { 315 if (isManualDnd() || !canEditPolicy()) { 316 throw new IllegalStateException("Cannot update interruption filter for mode " + this); 317 } 318 mRule.setInterruptionFilter(filter); 319 } 320 321 @NonNull getPolicy()322 public ZenPolicy getPolicy() { 323 switch (mRule.getInterruptionFilter()) { 324 case INTERRUPTION_FILTER_PRIORITY: 325 case NotificationManager.INTERRUPTION_FILTER_ALL: 326 return requireNonNull(mRule.getZenPolicy()); 327 328 case NotificationManager.INTERRUPTION_FILTER_ALARMS: 329 return new ZenPolicy.Builder(ZenModeConfig.getDefaultZenPolicy()).build() 330 .overwrittenWith(ZenPolicy.getBasePolicyInterruptionFilterAlarms()); 331 332 case NotificationManager.INTERRUPTION_FILTER_NONE: 333 return new ZenPolicy.Builder(ZenModeConfig.getDefaultZenPolicy()).build() 334 .overwrittenWith(ZenPolicy.getBasePolicyInterruptionFilterNone()); 335 336 case NotificationManager.INTERRUPTION_FILTER_UNKNOWN: 337 default: 338 Log.wtf(TAG, "Rule " + mId + " with unexpected interruptionFilter " 339 + mRule.getInterruptionFilter()); 340 return requireNonNull(mRule.getZenPolicy()); 341 } 342 } 343 344 /** 345 * Updates the {@link ZenPolicy} of the associated {@link AutomaticZenRule} based on the 346 * supplied policy. In some cases this involves conversions, so that the following call 347 * to {@link #getPolicy} might return a different policy from the one supplied here. 348 */ 349 @SuppressLint("WrongConstant") setPolicy(@onNull ZenPolicy policy)350 public void setPolicy(@NonNull ZenPolicy policy) { 351 if (!canEditPolicy()) { 352 throw new IllegalStateException("Cannot update ZenPolicy for mode " + this); 353 } 354 355 ZenPolicy currentPolicy = getPolicy(); 356 if (currentPolicy.equals(policy)) { 357 return; 358 } 359 360 if (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALL) { 361 Log.wtf(TAG, "Able to change policy without filtering being enabled"); 362 } 363 364 // If policy is customized from any of the "special" ones, make the rule PRIORITY. 365 if (mRule.getInterruptionFilter() != INTERRUPTION_FILTER_PRIORITY) { 366 mRule.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY); 367 } 368 mRule.setZenPolicy(policy); 369 } 370 371 /** 372 * Returns the {@link ZenDeviceEffects} of the mode. 373 * 374 * <p>This is never {@code null}; if the backing AutomaticZenRule doesn't have effects set then 375 * a default (empty) effects set is returned. 376 */ 377 @NonNull getDeviceEffects()378 public ZenDeviceEffects getDeviceEffects() { 379 return mRule.getDeviceEffects() != null 380 ? mRule.getDeviceEffects() 381 : new ZenDeviceEffects.Builder().build(); 382 } 383 384 /** Sets the {@link ZenDeviceEffects} of the mode. */ setDeviceEffects(@onNull ZenDeviceEffects effects)385 public void setDeviceEffects(@NonNull ZenDeviceEffects effects) { 386 checkNotNull(effects); 387 if (!canEditPolicy()) { 388 throw new IllegalStateException("Cannot update device effects for mode " + this); 389 } 390 mRule.setDeviceEffects(effects); 391 } 392 setCustomModeConditionId(Context context, Uri conditionId)393 public void setCustomModeConditionId(Context context, Uri conditionId) { 394 checkState(SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()), 395 "Trying to change condition of non-system-owned rule %s (to %s)", 396 mRule, conditionId); 397 398 Uri oldCondition = mRule.getConditionId(); 399 mRule.setConditionId(conditionId); 400 401 ZenModeConfig.ScheduleInfo scheduleInfo = tryParseScheduleConditionId(conditionId); 402 if (scheduleInfo != null) { 403 mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_TIME); 404 mRule.setOwner(ZenModeConfig.getScheduleConditionProvider()); 405 mRule.setTriggerDescription( 406 getTriggerDescriptionForScheduleTime(context, scheduleInfo)); 407 return; 408 } 409 410 ZenModeConfig.EventInfo eventInfo = tryParseEventConditionId(conditionId); 411 if (eventInfo != null) { 412 mRule.setType(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR); 413 mRule.setOwner(ZenModeConfig.getEventConditionProvider()); 414 mRule.setTriggerDescription(getTriggerDescriptionForScheduleEvent(context, eventInfo)); 415 return; 416 } 417 418 if (ZenModeConfig.isValidCustomManualConditionId(conditionId)) { 419 mRule.setType(AutomaticZenRule.TYPE_OTHER); 420 mRule.setOwner(ZenModeConfig.getCustomManualConditionProvider()); 421 mRule.setTriggerDescription(""); 422 return; 423 } 424 425 Log.wtf(TAG, String.format( 426 "Changed condition of rule %s (%s -> %s) but cannot recognize which kind of " 427 + "condition it was!", 428 mRule, oldCondition, conditionId)); 429 } 430 canEditNameAndIcon()431 public boolean canEditNameAndIcon() { 432 return !isManualDnd(); 433 } 434 435 /** 436 * Whether the mode has an editable policy. Calling {@link #setPolicy}, 437 * {@link #setDeviceEffects}, or {@link #setInterruptionFilter} is not valid for modes with a 438 * read-only policy. 439 */ canEditPolicy()440 public boolean canEditPolicy() { 441 // Cannot edit the policy of a temporarily active non-PRIORITY DND mode. 442 // Note that it's fine to edit the policy of an *AutomaticZenRule* with non-PRIORITY filter; 443 // the filter will we set to PRIORITY if you do. 444 return !isManualDndWithSpecialFilter(); 445 } 446 canBeDeleted()447 public boolean canBeDeleted() { 448 return !isManualDnd(); 449 } 450 isManualDnd()451 public boolean isManualDnd() { 452 return mKind == Kind.MANUAL_DND; 453 } 454 isManualDndWithSpecialFilter()455 private boolean isManualDndWithSpecialFilter() { 456 return isManualDnd() 457 && (mRule.getInterruptionFilter() == INTERRUPTION_FILTER_ALARMS 458 || mRule.getInterruptionFilter() == INTERRUPTION_FILTER_NONE); 459 } 460 461 /** 462 * A <em>custom manual</em> mode is a mode created by the user, and not yet assigned an 463 * automatic trigger condition (neither time schedule nor a calendar). 464 */ isCustomManual()465 public boolean isCustomManual() { 466 return isSystemOwned() 467 && getType() != TYPE_SCHEDULE_TIME 468 && getType() != TYPE_SCHEDULE_CALENDAR 469 && !isManualDnd(); 470 } 471 isEnabled()472 public boolean isEnabled() { 473 return mRule.isEnabled(); 474 } 475 476 /** 477 * Enables or disables the mode. 478 * 479 * <p>The DND mode cannot be disabled; trying to do so will fail. 480 */ setEnabled(boolean enabled)481 public void setEnabled(boolean enabled) { 482 if (isManualDnd()) { 483 throw new IllegalStateException("Cannot update enabled for manual DND mode " + this); 484 } 485 mRule.setEnabled(enabled); 486 } 487 isActive()488 public boolean isActive() { 489 return mStatus == Status.ENABLED_AND_ACTIVE; 490 } 491 isManualInvocationAllowed()492 public boolean isManualInvocationAllowed() { 493 return mRule.isManualInvocationAllowed(); 494 } 495 isSystemOwned()496 public boolean isSystemOwned() { 497 return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()); 498 } 499 500 @Override equals(@ullable Object obj)501 public boolean equals(@Nullable Object obj) { 502 return obj instanceof ZenMode other 503 && mId.equals(other.mId) 504 && mRule.equals(other.mRule) 505 && mKind.equals(other.mKind) 506 && mStatus.equals(other.mStatus); 507 } 508 509 @Override hashCode()510 public int hashCode() { 511 return Objects.hash(mId, mRule, mKind, mStatus); 512 } 513 514 @Override toString()515 public String toString() { 516 return mId + " (" + mKind + ", " + mStatus + ") -> " + mRule; 517 } 518 519 @Override describeContents()520 public int describeContents() { 521 return 0; 522 } 523 524 @Override writeToParcel(@onNull Parcel dest, int flags)525 public void writeToParcel(@NonNull Parcel dest, int flags) { 526 dest.writeString(mId); 527 dest.writeParcelable(mRule, 0); 528 dest.writeString(mKind.name()); 529 dest.writeString(mStatus.name()); 530 } 531 532 public static final Creator<ZenMode> CREATOR = new Creator<ZenMode>() { 533 @Override 534 public ZenMode createFromParcel(Parcel in) { 535 return new ZenMode( 536 in.readString(), 537 checkNotNull(in.readParcelable(AutomaticZenRule.class.getClassLoader(), 538 AutomaticZenRule.class)), 539 Kind.valueOf(in.readString()), 540 Status.valueOf(in.readString())); 541 } 542 543 @Override 544 public ZenMode[] newArray(int size) { 545 return new ZenMode[size]; 546 } 547 }; 548 } 549