diff --git a/res/values/strings.xml b/res/values/strings.xml index 4dab0b895a5..8c6b9db4740 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -7621,6 +7621,9 @@ Allow notification dots + + Allow notification bubbles + Blink light diff --git a/res/xml/app_notification_settings.xml b/res/xml/app_notification_settings.xml index 49d64a9731d..54d6fe7dcaf 100644 --- a/res/xml/app_notification_settings.xml +++ b/res/xml/app_notification_settings.xml @@ -60,9 +60,14 @@ settings:useAdditionalSummary="true" android:order="1001" settings:restrictedSwitchSummary="@string/enabled_by_admin" /> + diff --git a/res/xml/channel_notification_settings.xml b/res/xml/channel_notification_settings.xml index 9e9e7a96780..94a2cdb3d2c 100644 --- a/res/xml/channel_notification_settings.xml +++ b/res/xml/channel_notification_settings.xml @@ -86,17 +86,23 @@ settings:useAdditionalSummary="true" settings:restrictedSwitchSummary="@string/enabled_by_admin"/> + + diff --git a/res/xml/configure_notification_settings.xml b/res/xml/configure_notification_settings.xml index ca826095f08..8df1197e997 100644 --- a/res/xml/configure_notification_settings.xml +++ b/res/xml/configure_notification_settings.xml @@ -38,6 +38,12 @@ android:title="@string/notification_badging_title" settings:controller="com.android.settings.notification.BadgingNotificationPreferenceController"/> + + + (mControllers); } diff --git a/src/com/android/settings/notification/BadgePreferenceController.java b/src/com/android/settings/notification/BadgePreferenceController.java index ad417b0ad6a..67332a44bb4 100644 --- a/src/com/android/settings/notification/BadgePreferenceController.java +++ b/src/com/android/settings/notification/BadgePreferenceController.java @@ -60,7 +60,7 @@ public class BadgePreferenceController extends NotificationPreferenceController if (isDefaultChannel()) { return true; } else { - return mAppRow.showBadge; + return mAppRow == null ? false : mAppRow.showBadge; } } return true; diff --git a/src/com/android/settings/notification/BubbleNotificationPreferenceController.java b/src/com/android/settings/notification/BubbleNotificationPreferenceController.java new file mode 100644 index 00000000000..caba7d92cd9 --- /dev/null +++ b/src/com/android/settings/notification/BubbleNotificationPreferenceController.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2019 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.Secure.NOTIFICATION_BUBBLES; +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settings.core.TogglePreferenceController; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.OnPause; +import com.android.settingslib.core.lifecycle.events.OnResume; + +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +public class BubbleNotificationPreferenceController extends TogglePreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener, + LifecycleObserver, OnResume, OnPause { + + private static final String TAG = "BubbleNotifPrefContr"; + @VisibleForTesting + static final int ON = 1; + @VisibleForTesting + static final int OFF = 0; + + private SettingObserver mSettingObserver; + + public BubbleNotificationPreferenceController(Context context, String preferenceKey) { + super(context, preferenceKey); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + Preference preference = screen.findPreference(NOTIFICATION_BUBBLES); + if (preference != null) { + mSettingObserver = new SettingObserver(preference); + } + } + + @Override + public void onResume() { + if (mSettingObserver != null) { + mSettingObserver.register(mContext.getContentResolver(), true /* register */); + } + } + + @Override + public void onPause() { + if (mSettingObserver != null) { + mSettingObserver.register(mContext.getContentResolver(), false /* register */); + } + } + + @Override + public int getAvailabilityStatus() { + return AVAILABLE; + } + + @Override + public boolean isChecked() { + return Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, ON) == ON; + } + + @Override + public boolean setChecked(boolean isChecked) { + return Settings.Secure.putInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, isChecked ? ON : OFF); + } + + class SettingObserver extends ContentObserver { + + private final Uri NOTIFICATION_BUBBLES_URI = + Settings.Secure.getUriFor(NOTIFICATION_BUBBLES); + + private final Preference mPreference; + + public SettingObserver(Preference preference) { + super(new Handler()); + mPreference = preference; + } + + public void register(ContentResolver cr, boolean register) { + if (register) { + cr.registerContentObserver(NOTIFICATION_BUBBLES_URI, false, this); + } else { + cr.unregisterContentObserver(this); + } + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (NOTIFICATION_BUBBLES_URI.equals(uri)) { + updateState(mPreference); + } + } + } +} diff --git a/src/com/android/settings/notification/BubblePreferenceController.java b/src/com/android/settings/notification/BubblePreferenceController.java new file mode 100644 index 00000000000..e61de4ba40f --- /dev/null +++ b/src/com/android/settings/notification/BubblePreferenceController.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2019 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.Secure.NOTIFICATION_BUBBLES; + +import android.content.Context; +import android.provider.Settings; + +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.RestrictedSwitchPreference; + +import androidx.preference.Preference; + +public class BubblePreferenceController extends NotificationPreferenceController + implements PreferenceControllerMixin, Preference.OnPreferenceChangeListener { + + private static final String TAG = "BubblePrefContr"; + private static final String KEY = "bubble"; + private static final int SYSTEM_WIDE_ON = 1; + private static final int SYSTEM_WIDE_OFF = 0; + + public BubblePreferenceController(Context context, NotificationBackend backend) { + super(context, backend); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public boolean isAvailable() { + if (!super.isAvailable()) { + return false; + } + if (mAppRow == null && mChannel == null) { + return false; + } + if (Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, SYSTEM_WIDE_ON) == SYSTEM_WIDE_OFF) { + return false; + } + if (mChannel != null) { + if (isDefaultChannel()) { + return true; + } else { + return mAppRow == null ? false : mAppRow.allowBubbles; + } + } + return true; + } + + public void updateState(Preference preference) { + if (mAppRow != null) { + RestrictedSwitchPreference pref = (RestrictedSwitchPreference) preference; + pref.setDisabledByAdmin(mAdmin); + if (mChannel != null) { + pref.setChecked(mChannel.canBubble()); + pref.setEnabled(isChannelConfigurable() && !pref.isDisabledByAdmin()); + } else { + pref.setChecked(mAppRow.allowBubbles); + } + } + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean value = (Boolean) newValue; + if (mChannel != null) { + mChannel.setAllowBubbles(value); + saveChannel(); + } else if (mAppRow != null){ + mAppRow.allowBubbles = value; + mBackend.setAllowBubbles(mAppRow.pkg, mAppRow.uid, value); + } + return true; + } + +} diff --git a/src/com/android/settings/notification/ChannelNotificationSettings.java b/src/com/android/settings/notification/ChannelNotificationSettings.java index 0e1f386f4db..f92e529aab1 100644 --- a/src/com/android/settings/notification/ChannelNotificationSettings.java +++ b/src/com/android/settings/notification/ChannelNotificationSettings.java @@ -110,6 +110,7 @@ public class ChannelNotificationSettings extends NotificationSettingsBase { mControllers.add(new BadgePreferenceController(context, mBackend)); mControllers.add(new DndPreferenceController(context, mBackend)); mControllers.add(new NotificationsOffPreferenceController(context)); + mControllers.add(new BubblePreferenceController(context, mBackend)); return new ArrayList<>(mControllers); } } diff --git a/src/com/android/settings/notification/NotificationBackend.java b/src/com/android/settings/notification/NotificationBackend.java index ec563698be1..dbba6161d5a 100644 --- a/src/com/android/settings/notification/NotificationBackend.java +++ b/src/com/android/settings/notification/NotificationBackend.java @@ -71,6 +71,7 @@ public class NotificationBackend { row.icon = IconDrawableFactory.newInstance(context).getBadgedIcon(app); row.banned = getNotificationsBanned(row.pkg, row.uid); row.showBadge = canShowBadge(row.pkg, row.uid); + row.allowBubbles = canBubble(row.pkg, row.uid); row.userId = UserHandle.getUserId(row.uid); row.blockedChannelCount = getBlockedChannelCount(row.pkg, row.uid); row.channelCount = getChannelCount(row.pkg, row.uid); @@ -175,6 +176,26 @@ public class NotificationBackend { } } + public boolean canBubble(String pkg, int uid) { + try { + return sINM.areBubblesAllowedForPackage(pkg, uid); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public boolean setAllowBubbles(String pkg, int uid, boolean allow) { + try { + sINM.setBubblesAllowed(pkg, uid, allow); + return true; + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return false; + } + } + + public NotificationChannel getChannel(String pkg, int uid, String channelId) { if (channelId == null) { return null; @@ -416,6 +437,7 @@ public class NotificationBackend { public boolean lockedImportance; public String lockedChannelId; public boolean showBadge; + public boolean allowBubbles; public int userId; public int blockedChannelCount; public int channelCount; diff --git a/tests/robotests/src/com/android/settings/notification/BubbleNotificationPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BubbleNotificationPreferenceControllerTest.java new file mode 100644 index 00000000000..c4587afe132 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/BubbleNotificationPreferenceControllerTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2019 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.Secure.NOTIFICATION_BUBBLES; + +import static com.android.settings.notification.BadgingNotificationPreferenceController.OFF; +import static com.android.settings.notification.BadgingNotificationPreferenceController.ON; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.provider.Settings; + +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.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +@RunWith(RobolectricTestRunner.class) +public class BubbleNotificationPreferenceControllerTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private Context mContext; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PreferenceScreen mScreen; + + private BubbleNotificationPreferenceController mController; + private Preference mPreference; + + private static final String KEY_NOTIFICATION_BUBBLES = "notification_bubbles"; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new BubbleNotificationPreferenceController(mContext, + KEY_NOTIFICATION_BUBBLES); + mPreference = new Preference(RuntimeEnvironment.application); + mPreference.setKey(mController.getPreferenceKey()); + when(mScreen.findPreference(mPreference.getKey())).thenReturn(mPreference); + } + + @Test + public void display_shouldDisplay() { + assertThat(mPreference.isVisible()).isTrue(); + } + + @Test + public void updateState_preferenceSetCheckedWhenSettingIsOn() { + final TwoStatePreference preference = mock(TwoStatePreference.class); + final Context context = RuntimeEnvironment.application; + Settings.Secure.putInt(context.getContentResolver(), NOTIFICATION_BUBBLES, ON); + + mController.updateState(preference); + + verify(preference).setChecked(true); + } + + @Test + public void updateState_preferenceSetUncheckedWhenSettingIsOff() { + final TwoStatePreference preference = mock(TwoStatePreference.class); + final Context context = RuntimeEnvironment.application; + Settings.Secure.putInt(context.getContentResolver(), NOTIFICATION_BUBBLES, OFF); + + mController.updateState(preference); + + verify(preference).setChecked(false); + } + + @Test + public void isChecked_settingIsOff_shouldReturnFalse() { + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, OFF); + + assertThat(mController.isChecked()).isFalse(); + } + + @Test + public void isChecked_settingIsOn_shouldReturnTrue() { + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, ON); + + assertThat(mController.isChecked()).isTrue(); + } + + @Test + public void setChecked_setFalse_disablesSetting() { + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, ON); + + mController.setChecked(false); + int updatedValue = Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, -1); + + assertThat(updatedValue).isEqualTo(OFF); + } + + @Test + public void setChecked_setTrue_enablesSetting() { + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, OFF); + + mController.setChecked(true); + int updatedValue = Settings.Secure.getInt(mContext.getContentResolver(), + NOTIFICATION_BUBBLES, -1); + + assertThat(updatedValue).isEqualTo(ON); + } + + @Test + public void isSliceable_returnsFalse() { + assertThat(mController.isSliceable()).isFalse(); + } +} diff --git a/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java new file mode 100644 index 00000000000..99787d824bc --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/BubblePreferenceControllerTest.java @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2019 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.app.NotificationChannel.DEFAULT_CHANNEL_ID; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_LOW; +import static android.app.NotificationManager.IMPORTANCE_NONE; +import static android.provider.Settings.Secure.NOTIFICATION_BUBBLES; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.UserManager; +import android.provider.Settings; + +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedSwitchPreference; + +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.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; + +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +@RunWith(RobolectricTestRunner.class) +public class BubblePreferenceControllerTest { + + private Context mContext; + @Mock + private NotificationBackend mBackend; + @Mock + private NotificationManager mNm; + @Mock + private UserManager mUm; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private PreferenceScreen mScreen; + + private BubblePreferenceController mController; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + ShadowApplication shadowApplication = ShadowApplication.getInstance(); + shadowApplication.setSystemService(Context.NOTIFICATION_SERVICE, mNm); + shadowApplication.setSystemService(Context.USER_SERVICE, mUm); + mContext = RuntimeEnvironment.application; + mController = spy(new BubblePreferenceController(mContext, mBackend)); + } + + @Test + public void testNoCrashIfNoOnResume() { + mController.isAvailable(); + mController.updateState(mock(RestrictedSwitchPreference.class)); + mController.onPreferenceChange(mock(RestrictedSwitchPreference.class), true); + } + + @Test + public void testIsAvailable_notIfAppBlocked() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.banned = true; + mController.onResume(appRow, mock(NotificationChannel.class), null, null); + assertFalse(mController.isAvailable()); + } + + @Test + public void testIsAvailable_notIfChannelBlocked() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_NONE); + mController.onResume(appRow, channel, null, null); + assertFalse(mController.isAvailable()); + } + + @Test + public void testIsAvailable_channel_notIfAppOff() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = false; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH); + mController.onResume(appRow, channel, null, null); + + assertFalse(mController.isAvailable()); + } + + @Test + public void testIsAvailable_notIfOffGlobally() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH); + mController.onResume(appRow, channel, null, null); + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 0); + + assertFalse(mController.isAvailable()); + } + + @Test + public void testIsAvailable_app() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + mController.onResume(appRow, null, null, null); + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 1); + + assertTrue(mController.isAvailable()); + } + + @Test + public void testIsAvailable_defaultChannel() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH); + when(channel.getId()).thenReturn(DEFAULT_CHANNEL_ID); + mController.onResume(appRow, channel, null, null); + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 1); + + assertTrue(mController.isAvailable()); + } + + @Test + public void testIsAvailable_channel() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH); + mController.onResume(appRow, channel, null, null); + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 1); + + assertTrue(mController.isAvailable()); + } + + @Test + public void testIsAvailable_channelAppOff() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = false; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getImportance()).thenReturn(IMPORTANCE_HIGH); + mController.onResume(appRow, channel, null, null); + Settings.Secure.putInt(mContext.getContentResolver(), NOTIFICATION_BUBBLES, 1); + + assertFalse(mController.isAvailable()); + } + + @Test + public void testUpdateState_disabledByAdmin() { + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getId()).thenReturn("something"); + mController.onResume(new NotificationBackend.AppRow(), channel, null, + mock(RestrictedLockUtils.EnforcedAdmin.class)); + + Preference pref = new RestrictedSwitchPreference(mContext); + mController.updateState(pref); + + assertFalse(pref.isEnabled()); + } + + @Test + public void testUpdateState_channelNotConfigurable() { + String lockedId = "locked"; + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.lockedChannelId = lockedId; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.getId()).thenReturn(lockedId); + mController.onResume(appRow, channel, null, null); + + Preference pref = new RestrictedSwitchPreference(mContext); + mController.updateState(pref); + + assertFalse(pref.isEnabled()); + } + + @Test + public void testUpdateState_channel() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.lockedChannelId = "a"; + NotificationChannel channel = mock(NotificationChannel.class); + when(channel.canBubble()).thenReturn(true); + mController.onResume(appRow, channel, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + mController.updateState(pref); + + assertTrue(pref.isChecked()); + + when(channel.canBubble()).thenReturn(false); + mController.onResume(appRow, channel, null, null); + mController.updateState(pref); + + assertFalse(pref.isChecked()); + } + + @Test + public void testUpdateState_app() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + mController.onResume(appRow, null, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + mController.updateState(pref); + assertTrue(pref.isChecked()); + + appRow.allowBubbles = false; + mController.onResume(appRow, null, null, null); + + mController.updateState(pref); + assertFalse(pref.isChecked()); + } + + @Test + public void testOnPreferenceChange_on_channel() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + NotificationChannel channel = + new NotificationChannel(DEFAULT_CHANNEL_ID, "a", IMPORTANCE_LOW); + channel.setAllowBubbles(false); + mController.onResume(appRow, channel, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref); + mController.displayPreference(mScreen); + mController.updateState(pref); + + mController.onPreferenceChange(pref, true); + assertTrue(channel.canBubble()); + verify(mBackend, times(1)).updateChannel(any(), anyInt(), any()); + } + + @Test + public void testOnPreferenceChange_off_channel() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + NotificationChannel channel = + new NotificationChannel(DEFAULT_CHANNEL_ID, "a", IMPORTANCE_HIGH); + channel.setAllowBubbles(true); + mController.onResume(appRow, channel, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref); + mController.displayPreference(mScreen); + mController.updateState(pref); + + mController.onPreferenceChange(pref, false); + verify(mBackend, times(1)).updateChannel(any(), anyInt(), any()); + assertFalse(channel.canBubble()); + } + + @Test + public void testOnPreferenceChange_on_app() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = false; + mController.onResume(appRow, null, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref); + mController.displayPreference(mScreen); + mController.updateState(pref); + + mController.onPreferenceChange(pref, true); + + assertTrue(appRow.allowBubbles); + verify(mBackend, times(1)).setAllowBubbles(any(), anyInt(), eq(true)); + } + + @Test + public void testOnPreferenceChange_off_app() { + NotificationBackend.AppRow appRow = new NotificationBackend.AppRow(); + appRow.allowBubbles = true; + mController.onResume(appRow, null, null, null); + + RestrictedSwitchPreference pref = new RestrictedSwitchPreference(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(pref); + mController.displayPreference(mScreen); + mController.updateState(pref); + + mController.onPreferenceChange(pref, false); + + assertFalse(appRow.allowBubbles); + verify(mBackend, times(1)).setAllowBubbles(any(), anyInt(), eq(false)); + } +}