From b7b74226c306ab96afc2134579eba135ab21c9bf Mon Sep 17 00:00:00 2001 From: Beverly Date: Fri, 17 Nov 2017 16:29:30 -0500 Subject: [PATCH] Added footers to zen mode settings ZenModeSettings Footer displays when DND will end ZenModeBehaviorSettings Footer describes why dnd behavior cannot be changed when in alarms only or total silence mode Test: make RunSettingsRoboTests -j40 Bug: 63077372 Change-Id: Iefbb3995da4af2b210c8e0c3c3a798d3c613e275 --- res/layout/zen_mode_settings_button.xml | 1 - res/values/strings.xml | 20 +- res/xml/zen_mode_behavior_settings.xml | 3 +- res/xml/zen_mode_settings.xml | 4 + .../AbstractZenModePreferenceController.java | 78 +++++ ...odeBehaviorFooterPreferenceController.java | 101 ++++++ .../notification/ZenModeBehaviorSettings.java | 1 + .../notification/ZenModeSettings.java | 1 + ...odeSettingsFooterPreferenceController.java | 118 +++++++ ...ehaviorFooterPreferenceControllerTest.java | 247 ++++++++++++++ ...ettingsFooterPreferenceControllerTest.java | 317 ++++++++++++++++++ 11 files changed, 882 insertions(+), 9 deletions(-) create mode 100644 src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java create mode 100644 src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceControllerTest.java diff --git a/res/layout/zen_mode_settings_button.xml b/res/layout/zen_mode_settings_button.xml index 4d4b7d6cbfd..82989fc7863 100644 --- a/res/layout/zen_mode_settings_button.xml +++ b/res/layout/zen_mode_settings_button.xml @@ -19,7 +19,6 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:gravity="bottom" - android:paddingTop="4dp" android:paddingStart="72dp" android:paddingEnd="72dp" android:layout_width="match_parent" diff --git a/res/values/strings.xml b/res/values/strings.xml index 40152f73cb7..ab9e87807c8 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -6775,19 +6775,16 @@ TURN OFF NOW - Do Not Disturb is on until %s + Do Not Disturb is on until %s - Do Not Disturb will stay on until you turn it off. + Do Not Disturb will stay on until you turn it off - Do Not Disturb was automatically turned on by a rule %s + Do Not Disturb was automatically turned on by a rule (%s) - Do Not Disturb was automatically turned on by an app %s - - - Do Not Disturb was automatically turned on by a rule or app + Do Not Disturb was automatically turned on by an app (%s) Work profile sounds @@ -7205,6 +7202,15 @@ Configure rule + + These settings can\'t be changed right now. An app (%1$s) has automatically turned on Do Not Disturb with custom behavior." + + + These settings can\'t be changed right now. An app has automatically turned on Do Not Disturb with custom behavior." + + + These settings can\'t be changed right now. Do Not Disturb was manually turned on with custom behavior." + Time diff --git a/res/xml/zen_mode_behavior_settings.xml b/res/xml/zen_mode_behavior_settings.xml index 2536828966c..c0849daaf48 100644 --- a/res/xml/zen_mode_behavior_settings.xml +++ b/res/xml/zen_mode_behavior_settings.xml @@ -75,7 +75,8 @@ - + + diff --git a/res/xml/zen_mode_settings.xml b/res/xml/zen_mode_settings.xml index dbd1d42a4a8..0a6284d0b7c 100644 --- a/res/xml/zen_mode_settings.xml +++ b/res/xml/zen_mode_settings.xml @@ -41,4 +41,8 @@ android:layout="@layout/zen_mode_settings_button" /> + + + + diff --git a/src/com/android/settings/notification/AbstractZenModePreferenceController.java b/src/com/android/settings/notification/AbstractZenModePreferenceController.java index de2e8fd4c06..2642f8129e9 100644 --- a/src/com/android/settings/notification/AbstractZenModePreferenceController.java +++ b/src/com/android/settings/notification/AbstractZenModePreferenceController.java @@ -16,6 +16,9 @@ package com.android.settings.notification; +import android.app.ActivityManager; +import android.app.AlarmManager; +import android.app.AlarmManager.AlarmClockInfo; import android.app.NotificationManager; import android.content.ContentResolver; import android.content.Context; @@ -24,8 +27,11 @@ import android.net.Uri; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; +import android.service.notification.ScheduleCalendar; +import android.service.notification.ZenModeConfig; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; +import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.core.PreferenceControllerMixin; @@ -41,12 +47,15 @@ abstract public class AbstractZenModePreferenceController extends @VisibleForTesting protected SettingObserver mSettingObserver; + private final String KEY; final private NotificationManager mNotificationManager; + protected static ZenModeConfigWrapper mZenModeConfigWrapper; public AbstractZenModePreferenceController(Context context, String key, Lifecycle lifecycle) { super(context); + mZenModeConfigWrapper = new ZenModeConfigWrapper(context); if (lifecycle != null) { lifecycle.addObserver(this); } @@ -79,6 +88,10 @@ abstract public class AbstractZenModePreferenceController extends return mNotificationManager.getNotificationPolicy(); } + protected ZenModeConfig getZenModeConfig() { + return mNotificationManager.getZenModeConfig(); + } + protected int getZenMode() { return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.ZEN_MODE, 0); @@ -117,4 +130,69 @@ abstract public class AbstractZenModePreferenceController extends } } } + + /** + * Wrapper for testing compatibility + */ + @VisibleForTesting + static class ZenModeConfigWrapper { + private final Context mContext; + + public ZenModeConfigWrapper(Context context) { + mContext = context; + } + + protected String getOwnerCaption(String owner) { + return ZenModeConfig.getOwnerCaption(mContext, owner); + } + + protected boolean isTimeRule(Uri id) { + return ZenModeConfig.isValidEventConditionId(id) || + ZenModeConfig.isValidScheduleConditionId(id); + } + + protected CharSequence getFormattedTime(long time, int userHandle) { + return ZenModeConfig.getFormattedTime(mContext, time, isToday(time), userHandle); + } + + private boolean isToday(long time) { + return ZenModeConfig.isToday(time); + } + + protected long parseManualRuleTime(Uri id) { + return ZenModeConfig.tryParseCountdownConditionId(id); + } + + protected long parseAutomaticRuleEndTime(Uri id) { + if (ZenModeConfig.isValidEventConditionId(id)) { + // cannot look up end times for events + return Long.MAX_VALUE; + } + + if (ZenModeConfig.isValidScheduleConditionId(id)) { + ScheduleCalendar schedule = ZenModeConfig.toScheduleCalendar(id); + long endTimeMs = schedule.getNextChangeTime(System.currentTimeMillis()); + + // check if automatic rule will end on next alarm + if (schedule.exitAtAlarm()) { + long nextAlarm = getNextAlarm(mContext); + schedule.maybeSetNextAlarm(System.currentTimeMillis(), nextAlarm); + if (schedule.shouldExitForAlarm(endTimeMs)) { + return nextAlarm; + } + } + + + return endTimeMs; + } + + return -1; + } + } + + private static long getNextAlarm(Context context) { + final AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + final AlarmClockInfo info = alarms.getNextAlarmClock(ActivityManager.getCurrentUser()); + return info != null ? info.getTriggerTime() : 0; + } } diff --git a/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java b/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java new file mode 100644 index 00000000000..a1c2b017f17 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceController.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2017 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; + +import android.content.Context; +import android.content.ComponentName; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; +import android.support.v7.preference.Preference; +import android.util.Slog; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenModeBehaviorFooterPreferenceController extends AbstractZenModePreferenceController { + + protected static final String KEY = "footer_preference"; + + public ZenModeBehaviorFooterPreferenceController(Context context, Lifecycle lifecycle) { + super(context, KEY, lifecycle); + } + + @Override + public boolean isAvailable() { + return isDeprecatedZenMode(getZenMode()); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + boolean isAvailable = isAvailable(); + preference.setVisible(isAvailable); + if (isAvailable) { + preference.setTitle(getFooterText()); + } + + } + + protected String getFooterText() { + ZenModeConfig config = getZenModeConfig(); + + // DND turned on by manual rule with deprecated zen mode + if (config.manualRule != null && + isDeprecatedZenMode(config.manualRule.zenMode)) { + final Uri id = config.manualRule.conditionId; + if (config.manualRule.enabler != null) { + // app triggered manual rule + String appOwner = mZenModeConfigWrapper.getOwnerCaption(config.manualRule.enabler); + if (!appOwner.isEmpty()) { + return mContext.getString(R.string.zen_mode_app_set_behavior, appOwner); + } + } else { + return mContext.getString(R.string.zen_mode_qs_set_behavior); + } + } + + // DND turned on by an automatic rule with deprecated zen mode + for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { + if (automaticRule.isAutomaticActive() && isDeprecatedZenMode(automaticRule.zenMode)) { + ComponentName component = automaticRule.component; + if (component != null) { + return mContext.getString(R.string.zen_mode_app_set_behavior, + component.getPackageName()); + } + } + } + + return mContext.getString(R.string.zen_mode_unknown_app_set_behavior); + } + + private boolean isDeprecatedZenMode(int zenMode) { + switch (zenMode) { + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_ALARMS: + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/com/android/settings/notification/ZenModeBehaviorSettings.java b/src/com/android/settings/notification/ZenModeBehaviorSettings.java index b58ef86a547..bfa95a75703 100644 --- a/src/com/android/settings/notification/ZenModeBehaviorSettings.java +++ b/src/com/android/settings/notification/ZenModeBehaviorSettings.java @@ -48,6 +48,7 @@ public class ZenModeBehaviorSettings extends ZenModeSettingsBase implements Inde controllers.add(new ZenModeRepeatCallersPreferenceController(context, lifecycle)); controllers.add(new ZenModeScreenOnPreferenceController(context, lifecycle)); controllers.add(new ZenModeScreenOffPreferenceController(context, lifecycle)); + controllers.add(new ZenModeBehaviorFooterPreferenceController(context, lifecycle)); return controllers; } diff --git a/src/com/android/settings/notification/ZenModeSettings.java b/src/com/android/settings/notification/ZenModeSettings.java index 22d6fcad7ea..f8408fcd273 100644 --- a/src/com/android/settings/notification/ZenModeSettings.java +++ b/src/com/android/settings/notification/ZenModeSettings.java @@ -64,6 +64,7 @@ public class ZenModeSettings extends ZenModeSettingsBase { controllers.add(new ZenModeBehaviorPreferenceController(context, lifecycle)); controllers.add(new ZenModeAutomationPreferenceController(context)); controllers.add(new ZenModeButtonPreferenceController(context, lifecycle)); + controllers.add(new ZenModeSettingsFooterPreferenceController(context, lifecycle)); return controllers; } diff --git a/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java b/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java new file mode 100644 index 00000000000..752fe447300 --- /dev/null +++ b/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceController.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2017 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; + +import android.content.Context; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; +import android.support.v7.preference.Preference; + +import com.android.settings.R; +import com.android.settingslib.core.lifecycle.Lifecycle; + +public class ZenModeSettingsFooterPreferenceController extends AbstractZenModePreferenceController { + + protected static final String KEY = "footer_preference"; + + public ZenModeSettingsFooterPreferenceController(Context context, Lifecycle lifecycle) { + super(context, KEY, lifecycle); + } + + @Override + public boolean isAvailable() { + switch(getZenMode()) { + case Settings.Global.ZEN_MODE_ALARMS: + case Settings.Global.ZEN_MODE_NO_INTERRUPTIONS: + case Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS: + return true; + case Settings.Global.ZEN_MODE_OFF: + default: + return false; + } + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + boolean isAvailable = isAvailable(); + preference.setVisible(isAvailable); + if (isAvailable) { + preference.setTitle(getFooterText()); + } + } + + protected String getFooterText() { + ZenModeConfig config = getZenModeConfig(); + String footerText = ""; + long latestEndTime = -1; + + // DND turned on by manual rule + if (config.manualRule != null) { + final Uri id = config.manualRule.conditionId; + if (config.manualRule.enabler != null) { + // app triggered manual rule + String appOwner = mZenModeConfigWrapper.getOwnerCaption(config.manualRule.enabler); + if (!appOwner.isEmpty()) { + footerText = mContext.getString( + R.string.zen_mode_settings_dnd_automatic_rule_app, appOwner); + } + } else { + if (id == null) { + return mContext.getString( + R.string.zen_mode_settings_dnd_manual_indefinite); + } else { + latestEndTime = mZenModeConfigWrapper.parseManualRuleTime(id); + if (latestEndTime > 0) { + final CharSequence formattedTime = mZenModeConfigWrapper.getFormattedTime( + latestEndTime, mContext.getUserId()); + footerText = mContext.getString( + R.string.zen_mode_settings_dnd_manual_end_time, + formattedTime); + } + } + } + } + + // DND turned on by an automatic rule + for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) { + if (automaticRule.isAutomaticActive()) { + // set footer if 3rd party rule + if (!mZenModeConfigWrapper.isTimeRule(automaticRule.conditionId)) { + return mContext.getString(R.string.zen_mode_settings_dnd_automatic_rule, + automaticRule.name); + } else { + // set footer if automatic rule end time is the latest active rule end time + long endTime = mZenModeConfigWrapper.parseAutomaticRuleEndTime( + automaticRule.conditionId); + if (endTime > latestEndTime) { + latestEndTime = endTime; + footerText = mContext.getString( + R.string.zen_mode_settings_dnd_automatic_rule, automaticRule.name); + } + } + } + } + return footerText; + } +} diff --git a/tests/robotests/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceControllerTest.java new file mode 100644 index 00000000000..a7c051b7956 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/ZenModeBehaviorFooterPreferenceControllerTest.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2017 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; + +import static android.provider.Settings.Global.ZEN_MODE; +import static android.provider.Settings.Global.ZEN_MODE_ALARMS; +import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_OFF; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.NotificationManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ComponentName; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.util.ArrayMap; + +import com.android.settings.notification.AbstractZenModePreferenceController.ZenModeConfigWrapper; +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION_O) +public class ZenModeBehaviorFooterPreferenceControllerTest { + private ZenModeBehaviorFooterPreferenceController mController; + private final String TEST_APP_NAME = "test_app"; + private final String MANUAL_RULE_FIELD = "manualRule"; + private final String AUTOMATIC_RULES_FIELD = "automaticRules"; + + @Mock + private NotificationManager mNotificationManager; + @Mock + private Preference mockPref; + @Mock + private ZenModeConfig mZenModeConfig; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private ZenModeConfig mConfig; + @Mock + private ZenModeConfigWrapper mConfigWrapper; + + private Context mContext; + private ContentResolver mContentResolver; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowApplication = ShadowApplication.getInstance(); + shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNotificationManager); + + mContext = shadowApplication.getApplicationContext(); + mContentResolver = RuntimeEnvironment.application.getContentResolver(); + when(mNotificationManager.getZenModeConfig()).thenReturn(mZenModeConfig); + + mController = new ZenModeBehaviorFooterPreferenceController(mContext, + mock(Lifecycle.class)); + ReflectionHelpers.setField(mController, "mZenModeConfigWrapper", mConfigWrapper); + + when(mPreferenceScreen.findPreference(mController.getPreferenceKey())).thenReturn( + mockPref); + mController.displayPreference(mPreferenceScreen); + } + + @Test + public void totalSilence_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_NO_INTERRUPTIONS); + assertTrue(mController.isAvailable()); + } + + @Test + public void alarmsOnly_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_ALARMS); + assertTrue(mController.isAvailable()); + } + + @Test + public void priorityOnly_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertFalse(mController.isAvailable()); + } + + @Test + public void zenModeOff_footerIsNotAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_OFF); + assertFalse(mController.isAvailable()); + } + + @Test + public void zenModeOff_updateState_noFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_OFF); + mController.updateState(mockPref); + + verify(mockPref, never()).setTitle(any(String.class)); + } + + @Test + public void zenModeImportantInterruptions_updateState_noFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mController.updateState(mockPref); + + verify(mockPref, never()).setTitle(any(String.class)); + } + + @Test + public void deprecatedZenModeAlarms_qsManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_ALARMS); + + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_ALARMS; + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_qs_set_behavior)); + } + + @Test + public void deprecatedZenModeAlarms_appManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_ALARMS); + + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_ALARMS; + injectedManualRule.enabler = TEST_APP_NAME; + when(mConfigWrapper.getOwnerCaption(injectedManualRule.enabler)).thenReturn(TEST_APP_NAME); + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_app_set_behavior, TEST_APP_NAME)); + } + + @Test + public void deprecatedZenModeNoInterruptions_qsManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_NO_INTERRUPTIONS); + + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS; + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_qs_set_behavior)); + } + + @Test + public void deprecatedZenModeNoInterruptions_appManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_NO_INTERRUPTIONS); + + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS; + injectedManualRule.enabler = TEST_APP_NAME; + when(mConfigWrapper.getOwnerCaption(injectedManualRule.enabler)).thenReturn(TEST_APP_NAME); + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_app_set_behavior, TEST_APP_NAME)); + } + + @Test + public void deprecatedZenModeAlarms_automaticRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_ALARMS); + + ArrayMap injectedAutomaticRules = new ArrayMap<>(); + ZenRule injectedRule = spy(new ZenRule()); + injectedRule.zenMode = ZEN_MODE_ALARMS; + injectedRule.component = mock(ComponentName.class); + when(injectedRule.isAutomaticActive()).thenReturn(true); + when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); + injectedAutomaticRules.put("testid", injectedRule); + + ReflectionHelpers.setField(mZenModeConfig, AUTOMATIC_RULES_FIELD, injectedAutomaticRules); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_app_set_behavior, TEST_APP_NAME)); + } + + @Test + public void deprecatedZenModeNoInterruptions_automaticRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_NO_INTERRUPTIONS); + + ArrayMap injectedAutomaticRules = new ArrayMap<>(); + ZenRule injectedRule = spy(new ZenRule()); + injectedRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS; + injectedRule.component = mock(ComponentName.class); + when(injectedRule.isAutomaticActive()).thenReturn(true); + when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); + injectedAutomaticRules.put("testid", injectedRule); + + ReflectionHelpers.setField(mZenModeConfig, AUTOMATIC_RULES_FIELD, injectedAutomaticRules); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_app_set_behavior, TEST_APP_NAME)); + } + +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceControllerTest.java new file mode 100644 index 00000000000..eed8237d7ff --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/ZenModeSettingsFooterPreferenceControllerTest.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2017 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; + +import static android.provider.Settings.Global.ZEN_MODE; +import static android.provider.Settings.Global.ZEN_MODE_ALARMS; +import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_OFF; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertTrue; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.app.NotificationManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ComponentName; +import android.net.Uri; +import android.provider.Settings; +import android.service.notification.ZenModeConfig; +import android.service.notification.ZenModeConfig.ZenRule; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.util.ArrayMap; + +import com.android.settings.notification.AbstractZenModePreferenceController.ZenModeConfigWrapper; +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION_O) +public class ZenModeSettingsFooterPreferenceControllerTest { + private ZenModeSettingsFooterPreferenceController mController; + private final String TEST_APP_NAME = "test_app"; + private final String TEST_RULE_NAME = "test_rule_name"; + private final String MANUAL_RULE_FIELD = "manualRule"; + private final String AUTOMATIC_RULES_FIELD = "automaticRules"; + + private final ArrayMap mInjectedAutomaticRules = new ArrayMap<>(); + ; + + @Mock + private NotificationManager mNotificationManager; + @Mock + private Preference mockPref; + @Mock + private ZenModeConfig mZenModeConfig; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private ZenModeConfigWrapper mConfigWrapper; + + private Context mContext; + private ContentResolver mContentResolver; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowApplication = ShadowApplication.getInstance(); + shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNotificationManager); + + mContext = shadowApplication.getApplicationContext(); + mContentResolver = RuntimeEnvironment.application.getContentResolver(); + when(mNotificationManager.getZenModeConfig()).thenReturn(mZenModeConfig); + + mController = new ZenModeSettingsFooterPreferenceController(mContext, + mock(Lifecycle.class)); + ReflectionHelpers.setField(mZenModeConfig, AUTOMATIC_RULES_FIELD, mInjectedAutomaticRules); + ReflectionHelpers.setField(mController, "mZenModeConfigWrapper", mConfigWrapper); + + when(mPreferenceScreen.findPreference(mController.getPreferenceKey())).thenReturn( + mockPref); + mController.displayPreference(mPreferenceScreen); + } + + @Test + public void totalSilence_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_NO_INTERRUPTIONS); + assertTrue(mController.isAvailable()); + } + + @Test + public void alarmsOnly_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_ALARMS); + assertTrue(mController.isAvailable()); + } + + @Test + public void priorityOnly_footerIsAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertTrue(mController.isAvailable()); + } + + @Test + public void zenModeOff_footerIsNotAvailable() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_OFF); + assertFalse(mController.isAvailable()); + } + + @Test + public void app_manualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + injectManualRuleFromApp(); + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_automatic_rule_app, + TEST_APP_NAME)); + } + + @Test + public void time_manualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + String placeholder = "placeholder"; + injectManualRuleWithTimeCountdown(1000, placeholder); + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_manual_end_time, placeholder)); + } + + @Test + public void forever_manualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + injectManualRuleWithIndefiniteEnd(); + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_manual_indefinite)); + } + + @Test + public void automaticRule_noManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + // no manual rule + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, null); + + // adding automatic rule + injectNewAutomaticRule(TEST_RULE_NAME, true, false); + + mController.updateState(mockPref); + + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_automatic_rule, + TEST_RULE_NAME)); + } + + + @Test + public void manualRuleEndsLast_hasAutomaticRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + // manual rule that ends after automatic rule ends + injectManualRuleWithIndefiniteEnd(); + + // automatic rule that ends before manual rule ends + injectNewAutomaticRule(TEST_RULE_NAME, true, false); + + mController.updateState(mockPref); + + // manual rule end time is after automatic rule end time, so it is displayed + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_manual_indefinite)); + } + + + @Test + public void automaticRuleEndsLast_hasManualRule_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + // manual rule that ends before automatic rule ends + injectManualRuleWithTimeCountdown(1000, ""); + + // automatic rule that ends after manual rule ends + ZenRule rule = injectNewAutomaticRule(TEST_RULE_NAME, true, false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule.conditionId)).thenReturn( + (long) 2000); + + mController.updateState(mockPref); + + // automatic rule end time is after manual rule end time, so it is displayed + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_automatic_rule, + TEST_RULE_NAME)); + } + + @Test + public void multipleAutomaticRules_appAutoRuleautomaticRuleApp_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // automatic rule that ends after manual rule ends + ZenRule rule1 = injectNewAutomaticRule(TEST_RULE_NAME + "1", false, + false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule1.conditionId)).thenReturn( + (long) 10000); + + ZenRule rule2 = injectNewAutomaticRule(TEST_RULE_NAME + "2", true, + true); + + ZenRule rule3 = injectNewAutomaticRule(TEST_RULE_NAME + "3", true, + false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule3.conditionId)).thenReturn( + (long) 9000); + + mController.updateState(mockPref); + + // automatic rule from app is displayed + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_automatic_rule, + TEST_RULE_NAME + "2")); + } + + @Test + public void multipleAutomaticRules_setFooterTitle() { + Settings.Global.putInt(mContentResolver, ZEN_MODE, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // automatic rule that ends after manual rule ends + ZenRule rule1 = injectNewAutomaticRule(TEST_RULE_NAME + "1", true, + false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule1.conditionId)).thenReturn( + (long) 2000); + + ZenRule rule2 = injectNewAutomaticRule(TEST_RULE_NAME + "2", true, + false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule2.conditionId)).thenReturn( + (long) 8000); + + ZenRule rule3 = injectNewAutomaticRule(TEST_RULE_NAME + "3", false, + false); + when(mConfigWrapper.parseAutomaticRuleEndTime(rule3.conditionId)).thenReturn( + (long) 12000); + + mController.updateState(mockPref); + + // active automatic rule with the latest end time will display + verify(mockPref).setTitle(mContext.getString( + com.android.settings.R.string.zen_mode_settings_dnd_automatic_rule, + TEST_RULE_NAME + "2")); + } + + // manual rule that has no end condition (forever) + private void injectManualRuleWithIndefiniteEnd() { + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + injectedManualRule.conditionId = null; + injectedManualRule.enabler = null; + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + } + + // manual rule triggered by an app + private void injectManualRuleFromApp() { + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + injectedManualRule.enabler = TEST_APP_NAME; + when(mConfigWrapper.getOwnerCaption(injectedManualRule.enabler)).thenReturn(TEST_APP_NAME); + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + } + + // manual rule that ends in specified time + private void injectManualRuleWithTimeCountdown(long time, String timePlaceholder) { + ZenRule injectedManualRule = new ZenRule(); + injectedManualRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; + injectedManualRule.enabler = null; + injectedManualRule.conditionId = mock(Uri.class); + when(mConfigWrapper.parseManualRuleTime(injectedManualRule.conditionId)).thenReturn( + time); + when(mConfigWrapper.getFormattedTime(time, mContext.getUserId())).thenReturn( + timePlaceholder); + ReflectionHelpers.setField(mZenModeConfig, MANUAL_RULE_FIELD, injectedManualRule); + } + + // manual rule that ends in time + private ZenRule injectNewAutomaticRule(String nameAndId, boolean isActive, boolean isApp) { + ZenRule injectedRule = spy(new ZenRule()); + injectedRule.zenMode = ZEN_MODE_NO_INTERRUPTIONS; + injectedRule.component = mock(ComponentName.class); + injectedRule.name = nameAndId; + injectedRule.conditionId = new Uri.Builder().authority(nameAndId).build(); // unique uri + when(injectedRule.isAutomaticActive()).thenReturn(isActive); + when(mConfigWrapper.isTimeRule(injectedRule.conditionId)).thenReturn(!isApp); + if (isApp) { + when(injectedRule.component.getPackageName()).thenReturn(TEST_APP_NAME); + } + mInjectedAutomaticRules.put(nameAndId, injectedRule); + + return injectedRule; + } +}