Adds modes settings page for bypassing apps

Adds the settings page that a user will reach from a mode settings page
when they tap on "App," to allow  them to select whether all, none, or
priority modes can break through and send notifications.

Bug: 308819928
Bug: 331267485
Flag: android.app.modes_ui DEVELOPMENT
Test: build and test, controller unit tests
Change-Id: I5bcebfca0fc2882f839afdced8d2b817dad14e6c
This commit is contained in:
Alexander Roederer
2024-05-14 23:42:07 +00:00
parent aa6eb398fd
commit c512046eb9
11 changed files with 767 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2024 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.modes;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.Preference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModeAppsLinkPreferenceControllerTest {
private ZenModeAppsLinkPreferenceController mController;
private Context mContext;
@Mock
private ZenModesBackend mBackend;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mController = new ZenModeAppsLinkPreferenceController(
mContext, "controller_key", mBackend);
}
@Test
@EnableFlags(Flags.FLAG_MODES_UI)
public void testHasSummary() {
Preference pref = mock(Preference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build())
.build(), true);
mController.updateZenMode(pref, zenMode);
verify(pref).setSummary(any());
}
}

View File

@@ -0,0 +1,343 @@
/*
* Copyright (C) 2024 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.modes;
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_ALL;
import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_NONE;
import static com.android.settings.notification.modes.ZenModeAppsPreferenceController.KEY_PRIORITY;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.app.AutomaticZenRule;
import android.app.Flags;
import android.content.Context;
import android.net.Uri;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.ZenPolicy;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
import androidx.preference.TwoStatePreference;
import com.android.settingslib.widget.SelectorWithWidgetPreference;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
@RunWith(RobolectricTestRunner.class)
@EnableFlags(Flags.FLAG_MODES_UI)
public final class ZenModeAppsPreferenceControllerTest {
private Context mContext;
@Mock
private ZenModesBackend mBackend;
private ZenModeAppsPreferenceController mPriorityController;
private ZenModeAppsPreferenceController mAllController;
private ZenModeAppsPreferenceController mNoneController;
private SelectorWithWidgetPreference mPriorityPref;
private SelectorWithWidgetPreference mAllPref;
private SelectorWithWidgetPreference mNonePref;
private PreferenceCategory mPrefCategory;
private PreferenceScreen mPreferenceScreen;
@Rule
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
mPriorityController = new ZenModeAppsPreferenceController(mContext, KEY_PRIORITY, mBackend);
mNoneController = new ZenModeAppsPreferenceController(mContext, KEY_NONE, mBackend);
mAllController = new ZenModeAppsPreferenceController(mContext, KEY_ALL, mBackend);
mPriorityPref = makePreference(KEY_PRIORITY, mPriorityController);
mAllPref = makePreference(KEY_ALL, mAllController);
mNonePref = makePreference(KEY_NONE, mNoneController);
mPrefCategory = new PreferenceCategory(mContext);
mPrefCategory.setKey("zen_mode_apps_category");
PreferenceManager preferenceManager = new PreferenceManager(mContext);
mPreferenceScreen = preferenceManager.createPreferenceScreen(mContext);
mPreferenceScreen.addPreference(mPrefCategory);
mPrefCategory.addPreference(mPriorityPref);
mPrefCategory.addPreference(mAllPref);
mPrefCategory.addPreference(mNonePref);
mAllController.displayPreference(mPreferenceScreen);
mPriorityController.displayPreference(mPreferenceScreen);
mNoneController.displayPreference(mPreferenceScreen);
}
private SelectorWithWidgetPreference makePreference(String key,
ZenModeAppsPreferenceController controller) {
final SelectorWithWidgetPreference pref = new SelectorWithWidgetPreference(mContext, false);
pref.setKey(key);
pref.setOnClickListener(controller.mSelectorClickListener);
return pref;
}
@Test
public void testUpdateState_All() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenMode.CHANNEL_POLICY_ALL)
.build())
.build(), true);
mAllController.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testUpdateState_All_Unchecked() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build(), true);
mAllController.updateZenMode(preference, zenMode);
verify(preference).setChecked(false);
}
@Test
public void testUpdateState_None() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build(), true);
mNoneController.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testUpdateState_None_Unchecked() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenMode.CHANNEL_POLICY_ALL)
.build())
.build(), true);
mNoneController.updateZenMode(preference, zenMode);
verify(preference).setChecked(false);
}
@Test
public void testUpdateState_Priority() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
.build())
.build(), true);
mPriorityController.updateZenMode(preference, zenMode);
verify(preference).setChecked(true);
}
@Test
public void testUpdateState_Priority_Unchecked() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build(), true);
mPriorityController.updateZenMode(preference, zenMode);
verify(preference).setChecked(false);
}
@Test
public void testOnPreferenceChange_All() {
TwoStatePreference preference = mock(TwoStatePreference.class);
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setInterruptionFilter(INTERRUPTION_FILTER_NONE)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenMode.CHANNEL_POLICY_ALL)
.build())
.build(), true);
mAllController.updateZenMode(preference, zenMode);
mAllController.onPreferenceChange(preference, true);
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
assertThat(captor.getValue().getPolicy().getAllowedChannels())
.isEqualTo(ZenMode.CHANNEL_POLICY_ALL);
}
@Test
public void testPreferenceClick_passesCorrectCheckedState_All() {
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build(), true);
mAllController.updateZenMode(mAllPref, zenMode);
mNoneController.updateZenMode(mNonePref, zenMode);
mPriorityController.updateZenMode(mPriorityPref, zenMode);
// MPME is checked; ALL and PRIORITY are unchecked.
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
mPrefCategory.findPreference(KEY_ALL).performClick();
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
// Checks the policy value for ALL is set.
// The important part is that the interruption filter is propagated to the backend.
assertThat(captor.getValue().getRule().getInterruptionFilter())
.isEqualTo(INTERRUPTION_FILTER_ALL);
// ALL is now checked; others are unchecked.
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
}
@Test
public void testPreferenceClick_passesCorrectCheckedState_None() {
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
.build())
.build(), true);
mAllController.updateZenMode(mAllPref, zenMode);
mNoneController.updateZenMode(mNonePref, zenMode);
mPriorityController.updateZenMode(mPriorityPref, zenMode);
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
// Click on NONE
mPrefCategory.findPreference(KEY_NONE).performClick();
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
// NONE is not actually propagated to the backend as an interruption filter;
// the filter is set to priority, and sounds and visual effects are disallowed.
// See AbstractZenModePreferenceController.
assertThat(captor.getValue().getRule().getInterruptionFilter())
.isEqualTo(INTERRUPTION_FILTER_PRIORITY);
// NONE is now checked; others are unchecked.
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
}
@Test
public void testPreferenceClick_passesCorrectCheckedState_Priority() {
ZenMode zenMode = new ZenMode("id",
new AutomaticZenRule.Builder("Driving", Uri.parse("drive"))
.setType(AutomaticZenRule.TYPE_DRIVING)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build(), true);
mAllController.updateZenMode(mAllPref, zenMode);
mNoneController.updateZenMode(mNonePref, zenMode);
mPriorityController.updateZenMode(mPriorityPref, zenMode);
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
// Click on PRIORITY
mPrefCategory.findPreference(KEY_PRIORITY).performClick();
ArgumentCaptor<ZenMode> captor = ArgumentCaptor.forClass(ZenMode.class);
verify(mBackend).updateMode(captor.capture());
// Checks the policy value for PRIORITY is propagated to the backend.
assertThat(captor.getValue().getRule().getInterruptionFilter())
.isEqualTo(INTERRUPTION_FILTER_PRIORITY);
// PRIORITY is now checked; others are unchecked.
assertThat(((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_PRIORITY))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_ALL))
.isChecked());
assertThat(!((SelectorWithWidgetPreference) mPrefCategory.findPreference(KEY_NONE))
.isChecked());
}
}

View File

@@ -22,6 +22,7 @@ import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_AMBIENT;
import static android.service.notification.ZenPolicy.VISUAL_EFFECT_LIGHTS;
import static com.google.common.truth.Truth.assertThat;
import android.app.AutomaticZenRule;
@@ -29,6 +30,7 @@ import android.content.Context;
import android.net.Uri;
import android.service.notification.ZenDeviceEffects;
import android.service.notification.ZenPolicy;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -325,4 +327,46 @@ public class ZenModesSummaryHelperTest {
assertThat(mSummaryHelper.getDisplayEffectsSummary(zenMode)).isEqualTo(
"Notifications partially hidden, grayscale, and 2 more");
}
@Test
public void getAppsSummary_all() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenMode.CHANNEL_POLICY_ALL)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("All");
}
@Test
public void getAppsSummary_none() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_NONE)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("None");
}
@Test
public void getAppsSummary_priorityApps() {
AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed"))
.setType(AutomaticZenRule.TYPE_BEDTIME)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(new ZenPolicy.Builder()
.allowChannels(ZenPolicy.CHANNEL_POLICY_PRIORITY)
.build())
.build();
ZenMode zenMode = new ZenMode("id", rule, true);
assertThat(mSummaryHelper.getAppsSummary(zenMode)).isEqualTo("Selected apps");
}
}