diff options
7 files changed, 219 insertions, 21 deletions
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 6f94c1b2d274..4cbd5beb3a8c 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -3131,6 +3131,7 @@ public class ZenModeConfig implements Parcelable { * @return null if DND is off or describeForeverCondition is false and * DND is on forever (until turned off) */ + // TODO: b/368247671 - Delete when inlining MODES_UI public static String getDescription(Context context, boolean zenOn, ZenModeConfig config, boolean describeForeverCondition) { if (!zenOn || config == null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java index d5cfe55813ee..460c790174ab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java @@ -35,6 +35,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.SuppressLint; import android.app.AutomaticZenRule; import android.app.NotificationManager; +import android.content.ComponentName; import android.content.Context; import android.net.Uri; import android.os.Parcel; @@ -117,6 +118,14 @@ public class ZenMode implements Parcelable { DISABLED_BY_OTHER } + /** + * Information about the owner of a {@link ZenMode}. {@link #packageName()} is + * {@link SystemZenRules#PACKAGE_ANDROID} if the mode is system-owned; it may also be + * {@code null}, but only as an artifact of very old modes. + */ + public record Owner(@Nullable String packageName, @Nullable ComponentName configurationActivity, + @Nullable ComponentName conditionProvider) { } + private final String mId; private final AutomaticZenRule mRule; private final Kind mKind; @@ -198,7 +207,7 @@ public class ZenMode implements Parcelable { } @NonNull - public AutomaticZenRule getRule() { + AutomaticZenRule getRule() { return mRule; } @@ -207,6 +216,10 @@ public class ZenMode implements Parcelable { return Strings.nullToEmpty(mRule.getName()); } + public void setName(@NonNull String name) { + mRule.setName(name); + } + @NonNull public Kind getKind() { return mKind; @@ -217,6 +230,17 @@ public class ZenMode implements Parcelable { return mStatus; } + @NonNull + public Owner getOwner() { + return new Owner(mRule.getPackageName(), mRule.getConfigurationActivity(), + mRule.getOwner()); + } + + @Nullable + public String getOwnerPackage() { + return getOwner().packageName(); + } + @AutomaticZenRule.Type public int getType() { return mRule.getType(); @@ -257,6 +281,26 @@ public class ZenMode implements Parcelable { } } + /** + * Returns the resource id of the icon for this mode. Note that this is the <em>stored</em> + * resource id, and thus can be different from the value in {@link #getIconKey()} -- in + * particular, for modes without a custom icon set, this method returns {@code 0} whereas + * {@link #getIconKey()} will return a default icon based on other mode properties. + * + * <p>Most callers are interested in {@link #getIconKey()}, unless they are editing the icon. + */ + public int getIconResId() { + return mRule.getIconResId(); + } + + /** + * Sets the resource id of the icon for this mode. + * @see #getIconResId() + */ + public void setIconResId(@DrawableRes int iconResId) { + mRule.setIconResId(iconResId); + } + /** Returns the interruption filter of the mode. */ @NotificationManager.InterruptionFilter public int getInterruptionFilter() { @@ -445,6 +489,10 @@ public class ZenMode implements Parcelable { return mStatus == Status.ENABLED_AND_ACTIVE; } + public boolean isManualInvocationAllowed() { + return mRule.isManualInvocationAllowed(); + } + public boolean isSystemOwned() { return SystemZenRules.PACKAGE_ANDROID.equals(mRule.getPackageName()); } diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java index f5776989917e..b67339bace67 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeDescriptions.java @@ -70,8 +70,7 @@ public final class ZenModeDescriptions { public String getTriggerDescriptionForAccessibility(@NonNull ZenMode mode) { // Only one special case: time-based schedules, where we want to use full day names. if (mode.isSystemOwned() && mode.getType() == TYPE_SCHEDULE_TIME) { - ZenModeConfig.ScheduleInfo schedule = ZenModeConfig.tryParseScheduleConditionId( - mode.getRule().getConditionId()); + ZenModeConfig.ScheduleInfo schedule = ZenModeSchedules.getTimeSchedule(mode); if (schedule != null) { String fullDaysSummary = SystemZenRules.getDaysOfWeekFull(mContext, schedule); if (fullDaysSummary != null) { diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java new file mode 100644 index 000000000000..c981652f231d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModeSchedules.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 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.settingslib.notification.modes; + +import android.service.notification.ZenModeConfig; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ZenModeSchedules { + + /** + * Returns the {@link ZenModeConfig.ScheduleInfo} time schedule corresponding to the mode, or + * {@code null} if the mode is not time-schedule-based. + */ + @Nullable + public static ZenModeConfig.ScheduleInfo getTimeSchedule(@NonNull ZenMode mode) { + return ZenModeConfig.tryParseScheduleConditionId(mode.getRule().getConditionId()); + } + + /** + * Returns the {@link ZenModeConfig.EventInfo} calendar schedule corresponding to the mode, or + * {@code null} if the mode is not calendar-schedule-based. + */ + @Nullable + public static ZenModeConfig.EventInfo getCalendarSchedule(@NonNull ZenMode mode) { + return ZenModeConfig.tryParseEventConditionId(mode.getRule().getConditionId()); + } + + private ZenModeSchedules() { } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java index 6b30f159129e..71ecf5b76296 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModeTest.java @@ -21,6 +21,7 @@ import static android.app.AutomaticZenRule.TYPE_DRIVING; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; import static android.app.AutomaticZenRule.TYPE_OTHER; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_CALENDAR; +import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_THEATER; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; import static android.app.NotificationManager.INTERRUPTION_FILTER_ALARMS; @@ -35,6 +36,8 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import android.app.AutomaticZenRule; +import android.content.ComponentName; +import android.content.Context; import android.net.Uri; import android.os.Parcel; import android.service.notification.Condition; @@ -43,13 +46,17 @@ import android.service.notification.ZenDeviceEffects; import android.service.notification.ZenModeConfig; import android.service.notification.ZenPolicy; +import androidx.test.core.app.ApplicationProvider; + import com.android.internal.R; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; @RunWith(RobolectricTestRunner.class) @@ -72,6 +79,13 @@ public class ZenModeTest { .setType(TYPE_OTHER) .build(); + private Context mContext; + + @Before + public void setUp() { + mContext = ApplicationProvider.getApplicationContext(); + } + @Test public void testBasicMethods_mode() { ZenMode zenMode = new ZenMode("id", ZEN_RULE, zenConfigRuleFor(ZEN_RULE, true)); @@ -95,7 +109,7 @@ public class ZenModeTest { assertThat(manualMode.canEditPolicy()).isTrue(); assertThat(manualMode.canBeDeleted()).isFalse(); assertThat(manualMode.isActive()).isFalse(); - assertThat(manualMode.getRule().getPackageName()).isEqualTo(PACKAGE_ANDROID); + assertThat(manualMode.getOwnerPackage()).isEqualTo(PACKAGE_ANDROID); } @Test @@ -153,7 +167,7 @@ public class ZenModeTest { public void isCustomManual_scheduleTime_false() { AutomaticZenRule rule = new AutomaticZenRule.Builder("Mode", Uri.parse("x")) .setPackage(SystemZenRules.PACKAGE_ANDROID) - .setType(AutomaticZenRule.TYPE_SCHEDULE_TIME) + .setType(TYPE_SCHEDULE_TIME) .build(); ZenMode mode = new ZenMode("id", rule, zenConfigRuleFor(rule, false)); @@ -194,6 +208,23 @@ public class ZenModeTest { } @Test + public void getOwner_returnsOwnerDetails() { + AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) + .setPackage("package") + .setConfigurationActivity(new ComponentName("package", "configActivity")) + .setOwner(new ComponentName("package", "conditionService")) + .build(); + ZenMode zenMode = new ZenMode("id", azr, zenConfigRuleFor(azr, false)); + + ZenMode.Owner owner = zenMode.getOwner(); + assertThat(owner.packageName()).isEqualTo("package"); + assertThat(owner.configurationActivity()).isEqualTo( + new ComponentName("package", "configActivity")); + assertThat(owner.conditionProvider()).isEqualTo( + new ComponentName("package", "conditionService")); + } + + @Test public void getPolicy_interruptionFilterPriority_returnsZenPolicy() { AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) @@ -245,7 +276,7 @@ public class ZenModeTest { zenMode.setPolicy(ZEN_POLICY); - assertThat(zenMode.getRule().getInterruptionFilter()).isEqualTo( + assertThat(zenMode.getInterruptionFilter()).isEqualTo( INTERRUPTION_FILTER_PRIORITY); assertThat(zenMode.getPolicy()).isEqualTo(ZEN_POLICY); assertThat(zenMode.getRule().getZenPolicy()).isEqualTo(ZEN_POLICY); @@ -397,6 +428,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isEqualTo("some.package"); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test @@ -411,6 +443,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test @@ -425,15 +458,18 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isEqualTo("some.package"); assertThat(iconKey.resId()).isEqualTo(123); + assertThat(mode.getIconResId()).isEqualTo(123); } @Test public void getIconKey_manualDnd_isDndIcon() { - ZenIcon.Key iconKey = TestModeBuilder.MANUAL_DND.getIconKey(); + ZenMode mode = TestModeBuilder.MANUAL_DND; + ZenIcon.Key iconKey = mode.getIconKey(); assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_special_dnd); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -448,6 +484,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_bedtime); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -462,6 +499,7 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_schedule_calendar); + assertThat(mode.getIconResId()).isEqualTo(0); } @Test @@ -477,6 +515,77 @@ public class ZenModeTest { assertThat(iconKey.resPackage()).isNull(); assertThat(iconKey.resId()).isEqualTo( com.android.internal.R.drawable.ic_zen_mode_type_special_dnd); + assertThat(mode.getIconResId()).isEqualTo(0); + } + + @Test + public void setCustomModeConditionId_timeSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + ZenModeConfig.ScheduleInfo timeSchedule = new ZenModeConfig.ScheduleInfo(); + timeSchedule.startHour = 9; + timeSchedule.endHour = 12; + timeSchedule.days = new int[] {Calendar.SATURDAY, Calendar.SUNDAY}; + Uri scheduleUri = ZenModeConfig.toScheduleConditionId(timeSchedule); + + mode.setCustomModeConditionId(mContext, scheduleUri); + + assertThat(mode.getType()).isEqualTo(TYPE_SCHEDULE_TIME); + assertThat(ZenModeSchedules.getTimeSchedule(mode)).isEqualTo(timeSchedule); + assertThat(mode.getTriggerDescription()).isEqualTo("Sun, Sat, 9:00 AM - 12:00 PM"); + + assertThat(mode.getRule().getConditionId()).isEqualTo(scheduleUri); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getScheduleConditionProvider()); + } + + @Test + public void setCustomModeConditionId_calendarSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + ZenModeConfig.EventInfo calendarSchedule = new ZenModeConfig.EventInfo(); + calendarSchedule.calendarId = 1L; + calendarSchedule.calName = "My events"; + Uri scheduleUri = ZenModeConfig.toEventConditionId(calendarSchedule); + + mode.setCustomModeConditionId(mContext, scheduleUri); + + assertThat(mode.getType()).isEqualTo(TYPE_SCHEDULE_CALENDAR); + assertThat(ZenModeSchedules.getCalendarSchedule(mode)).isEqualTo(calendarSchedule); + assertThat(mode.getTriggerDescription()).isEqualTo("My events"); + + assertThat(mode.getRule().getConditionId()).isEqualTo(scheduleUri); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getEventConditionProvider()); + } + + @Test + public void setCustomModeConditionId_manualSchedule() { + ZenMode mode = new TestModeBuilder() + .setPackage(PACKAGE_ANDROID) + .build(); + + mode.setCustomModeConditionId(mContext, ZenModeConfig.toCustomManualConditionId()); + + assertThat(mode.getType()).isEqualTo(TYPE_OTHER); + assertThat(mode.getTriggerDescription()).isEqualTo(""); + + assertThat(mode.getRule().getConditionId()).isEqualTo( + ZenModeConfig.toCustomManualConditionId()); + assertThat(mode.getRule().getOwner()).isEqualTo( + ZenModeConfig.getCustomManualConditionProvider()); + } + + @Test + public void setCustomModeConditionId_nonSystemRule_throws() { + ZenMode mode = new TestModeBuilder() + .setPackage("some.other.package") + .build(); + + assertThrows(IllegalStateException.class, + () -> mode.setCustomModeConditionId(mContext, Uri.parse("blah"))); } private static void assertUnparceledIsEqualToOriginal(String type, ZenMode original) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt index 33ed419afef2..5dbe9b145bc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogEventLogger.kt @@ -21,29 +21,25 @@ import com.android.settingslib.notification.modes.ZenMode import com.android.systemui.qs.QSModesEvent import javax.inject.Inject -class ModesDialogEventLogger -@Inject -constructor( - private val uiEventLogger: UiEventLogger, -) { +class ModesDialogEventLogger @Inject constructor(private val uiEventLogger: UiEventLogger) { fun logModeOn(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_ON else QSModesEvent.QS_MODES_MODE_ON - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logModeOff(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_OFF else QSModesEvent.QS_MODES_MODE_OFF - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logModeSettings(mode: ZenMode) { val id = if (mode.isManualDnd) QSModesEvent.QS_MODES_DND_SETTINGS else QSModesEvent.QS_MODES_MODE_SETTINGS - uiEventLogger.log(id, /* uid= */ 0, mode.rule.packageName) + uiEventLogger.log(id, /* uid= */ 0, mode.ownerPackage) } fun logOpenDurationDialog(mode: ZenMode) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt index 07f1c3470c83..dc07202dc486 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt @@ -71,10 +71,10 @@ constructor( mode.id in prevIds -> true // Mode is enabled -> show if active (so user can toggle off), or if it // can be manually toggled on - mode.rule.isEnabled -> mode.isActive || mode.rule.isManualInvocationAllowed + mode.isEnabled -> mode.isActive || mode.isManualInvocationAllowed // Mode was created as disabled, or disabled by the app that owns it -> // will be shown with a "Not set" text - !mode.rule.isEnabled -> mode.status == ZenMode.Status.DISABLED_BY_OTHER + !mode.isEnabled -> mode.status == ZenMode.Status.DISABLED_BY_OTHER else -> false } } @@ -97,13 +97,13 @@ constructor( if (mode.isActive) R.string.zen_mode_on else R.string.zen_mode_off ), onClick = { - if (!mode.rule.isEnabled) { + if (!mode.isEnabled) { openSettings(mode) } else if (mode.isActive) { dialogEventLogger.logModeOff(mode) zenModeInteractor.deactivateMode(mode) } else { - if (mode.rule.isManualInvocationAllowed) { + if (mode.isManualInvocationAllowed) { if (zenModeInteractor.shouldAskForZenDuration(mode)) { dialogEventLogger.logOpenDurationDialog(mode) // NOTE: The dialog handles turning on the mode itself. @@ -144,10 +144,10 @@ constructor( * readers, and for the tile subtext will be augmented with the current status of the mode. */ private fun getModeDescription(mode: ZenMode, forAccessibility: Boolean): String? { - if (!mode.rule.isEnabled) { + if (!mode.isEnabled) { return context.resources.getString(R.string.zen_mode_set_up) } - if (!mode.rule.isManualInvocationAllowed && !mode.isActive) { + if (!mode.isManualInvocationAllowed && !mode.isActive) { return context.resources.getString(R.string.zen_mode_no_manual_invocation) } return if (forAccessibility) |