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; + } +}