From 5bfe1d3f9c8900e9ab196767fefe183a19389db1 Mon Sep 17 00:00:00 2001 From: Jeff DeCew Date: Tue, 18 Aug 2020 10:59:35 -0400 Subject: [PATCH] Update NotificationChannelSettings Preferences in place to prevent re-layout. (cherry picked from commit b409b640451cfc58fb529659b80cd872ed717095) Fixes: 110093185 Test: ChannelListPreferenceControllerTest Change-Id: If6acf305c44085e502a3304ea57e409ce049b40f Merged-In: If6acf305c44085e502a3304ea57e409ce049b40f --- .../app/ChannelListPreferenceController.java | 279 +++++++++---- .../widget/MasterSwitchPreference.java | 12 + .../ChannelListPreferenceControllerTest.java | 395 ++++++++++++++++++ 3 files changed, 600 insertions(+), 86 deletions(-) create mode 100644 tests/unit/src/com/android/settings/notification/app/ChannelListPreferenceControllerTest.java diff --git a/src/com/android/settings/notification/app/ChannelListPreferenceController.java b/src/com/android/settings/notification/app/ChannelListPreferenceController.java index 8a34672eea1..88d960d7dc9 100644 --- a/src/com/android/settings/notification/app/ChannelListPreferenceController.java +++ b/src/com/android/settings/notification/app/ChannelListPreferenceController.java @@ -23,16 +23,14 @@ import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.settings.SettingsEnums; import android.content.Context; -import android.graphics.BlendMode; -import android.graphics.BlendModeColorFilter; import android.graphics.drawable.Drawable; -import android.graphics.drawable.GradientDrawable; -import android.graphics.drawable.LayerDrawable; import android.os.AsyncTask; import android.os.Bundle; import android.provider.Settings; import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceGroup; @@ -53,7 +51,8 @@ import java.util.List; public class ChannelListPreferenceController extends NotificationPreferenceController { private static final String KEY = "channels"; - private static String KEY_GENERAL_CATEGORY = "categories"; + private static final String KEY_GENERAL_CATEGORY = "categories"; + private static final String KEY_ZERO_CATEGORIES = "zeroCategories"; public static final String ARG_FROM_SETTINGS = "fromSettings"; private List mChannelGroupList; @@ -102,62 +101,192 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr if (mContext == null) { return; } - populateList(); + updateFullList(mPreference, mChannelGroupList); } }.execute(); } - private void populateList() { - // TODO: if preference has children, compare with newly loaded list - mPreference.removeAll(); - - if (mChannelGroupList.isEmpty()) { - PreferenceCategory groupCategory = new PreferenceCategory(mContext); - groupCategory.setTitle(R.string.notification_channels); - groupCategory.setKey(KEY_GENERAL_CATEGORY); - mPreference.addPreference(groupCategory); - - Preference empty = new Preference(mContext); - empty.setTitle(R.string.no_channels); - empty.setEnabled(false); - groupCategory.addPreference(empty); - } else { - populateGroupList(); - } - } - - private void populateGroupList() { - for (NotificationChannelGroup group : mChannelGroupList) { - PreferenceCategory groupCategory = new PreferenceCategory(mContext); - groupCategory.setOrderingAsAdded(true); - mPreference.addPreference(groupCategory); - if (group.getId() == null) { - groupCategory.setTitle(R.string.notification_channels_other); - groupCategory.setKey(KEY_GENERAL_CATEGORY); + /** + * Update the preferences group to match the + * @param groupPrefsList + * @param channelGroups + */ + void updateFullList(@NonNull PreferenceCategory groupPrefsList, + @NonNull List channelGroups) { + if (channelGroups.isEmpty()) { + if (groupPrefsList.getPreferenceCount() == 1 + && KEY_ZERO_CATEGORIES.equals(groupPrefsList.getPreference(0).getKey())) { + // Ensure the titles are correct for the current language, but otherwise leave alone + PreferenceGroup groupCategory = (PreferenceGroup) groupPrefsList.getPreference(0); + groupCategory.setTitle(R.string.notification_channels); + groupCategory.getPreference(0).setTitle(R.string.no_channels); } else { - groupCategory.setTitle(group.getName()); - groupCategory.setKey(group.getId()); - populateGroupToggle(groupCategory, group); + // Clear any contents and create the 'zero-categories' group. + groupPrefsList.removeAll(); + + PreferenceCategory groupCategory = new PreferenceCategory(mContext); + groupCategory.setTitle(R.string.notification_channels); + groupCategory.setKey(KEY_ZERO_CATEGORIES); + groupPrefsList.addPreference(groupCategory); + + Preference empty = new Preference(mContext); + empty.setTitle(R.string.no_channels); + empty.setEnabled(false); + groupCategory.addPreference(empty); } - if (!group.isBlocked()) { - final List channels = group.getChannels(); - Collections.sort(channels, CHANNEL_COMPARATOR); - int N = channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel channel = channels.get(i); - // conversations get their own section - if (TextUtils.isEmpty(channel.getConversationId()) || channel.isDemoted()) { - populateSingleChannelPrefs(groupCategory, channel, group.isBlocked()); - } - } + } else { + updateGroupList(groupPrefsList, channelGroups); + } + } + + /** + * Looks for the category for the given group's key at the expected index, if that doesn't + * match, it checks all groups, and if it can't find that group anywhere, it creates it. + */ + @NonNull + private PreferenceCategory findOrCreateGroupCategoryForKey( + @NonNull PreferenceCategory groupPrefsList, @Nullable String key, int expectedIndex) { + if (key == null) { + key = KEY_GENERAL_CATEGORY; + } + int preferenceCount = groupPrefsList.getPreferenceCount(); + if (expectedIndex < preferenceCount) { + Preference preference = groupPrefsList.getPreference(expectedIndex); + if (key.equals(preference.getKey())) { + return (PreferenceCategory) preference; + } + } + for (int i = 0; i < preferenceCount; i++) { + Preference preference = groupPrefsList.getPreference(i); + if (key.equals(preference.getKey())) { + preference.setOrder(expectedIndex); + return (PreferenceCategory) preference; + } + } + PreferenceCategory groupCategory = new PreferenceCategory(mContext); + groupCategory.setOrder(expectedIndex); + groupCategory.setKey(key); + groupPrefsList.addPreference(groupCategory); + return groupCategory; + } + + private void updateGroupList(@NonNull PreferenceCategory groupPrefsList, + @NonNull List channelGroups) { + // Update the list, but optimize for the most common case where the list hasn't changed. + int numFinalGroups = channelGroups.size(); + int initialPrefCount = groupPrefsList.getPreferenceCount(); + List finalOrderedGroups = new ArrayList<>(numFinalGroups); + for (int i = 0; i < numFinalGroups; i++) { + NotificationChannelGroup group = channelGroups.get(i); + PreferenceCategory groupCategory = + findOrCreateGroupCategoryForKey(groupPrefsList, group.getId(), i); + finalOrderedGroups.add(groupCategory); + updateGroupPreferences(group, groupCategory); + } + int postAddPrefCount = groupPrefsList.getPreferenceCount(); + // If any groups were inserted (into a non-empty list) or need to be removed, we need to + // remove all groups and re-add them all. + // This is required to ensure proper ordering of inserted groups, and it simplifies logic + // at the cost of computation in the rare case that the list is changing. + boolean hasInsertions = initialPrefCount != 0 && initialPrefCount != numFinalGroups; + boolean requiresRemoval = postAddPrefCount != numFinalGroups; + if (hasInsertions || requiresRemoval) { + groupPrefsList.removeAll(); + for (PreferenceCategory group : finalOrderedGroups) { + groupPrefsList.addPreference(group); } } } - protected void populateGroupToggle(final PreferenceGroup parent, - NotificationChannelGroup group) { - RestrictedSwitchPreference preference = - new RestrictedSwitchPreference(mContext); + /** + * Looks for the channel preference for the given channel's key at the expected index, if that + * doesn't match, it checks all rows, and if it can't find that channel anywhere, it creates + * the preference. + */ + @NonNull + private MasterSwitchPreference findOrCreateChannelPrefForKey( + @NonNull PreferenceGroup groupPrefGroup, @NonNull String key, int expectedIndex) { + int preferenceCount = groupPrefGroup.getPreferenceCount(); + if (expectedIndex < preferenceCount) { + Preference preference = groupPrefGroup.getPreference(expectedIndex); + if (key.equals(preference.getKey())) { + return (MasterSwitchPreference) preference; + } + } + for (int i = 0; i < preferenceCount; i++) { + Preference preference = groupPrefGroup.getPreference(i); + if (key.equals(preference.getKey())) { + preference.setOrder(expectedIndex); + return (MasterSwitchPreference) preference; + } + } + MasterSwitchPreference channelPref = new MasterSwitchPreference(mContext); + channelPref.setOrder(expectedIndex); + channelPref.setKey(key); + groupPrefGroup.addPreference(channelPref); + return channelPref; + } + + private void updateGroupPreferences(@NonNull NotificationChannelGroup group, + @NonNull PreferenceGroup groupPrefGroup) { + int initialPrefCount = groupPrefGroup.getPreferenceCount(); + List finalOrderedPrefs = new ArrayList<>(); + if (group.getId() == null) { + // For the 'null' group, set the "Other" title. + groupPrefGroup.setTitle(R.string.notification_channels_other); + } else { + // For an app-defined group, set their name and create a row to toggle 'isBlocked'. + groupPrefGroup.setTitle(group.getName()); + finalOrderedPrefs.add(addOrUpdateGroupToggle(groupPrefGroup, group)); + } + // Here "empty" means having no channel rows; the group toggle is ignored for this purpose. + boolean initiallyEmpty = groupPrefGroup.getPreferenceCount() == finalOrderedPrefs.size(); + + // For each channel, add or update the preference object. + final List channels = + group.isBlocked() ? Collections.emptyList() : group.getChannels(); + Collections.sort(channels, CHANNEL_COMPARATOR); + for (NotificationChannel channel : channels) { + if (!TextUtils.isEmpty(channel.getConversationId()) && !channel.isDemoted()) { + // conversations get their own section + continue; + } + // Get or create the row, and populate its current state. + MasterSwitchPreference channelPref = findOrCreateChannelPrefForKey(groupPrefGroup, + channel.getId(), /* expectedIndex */ finalOrderedPrefs.size()); + updateSingleChannelPrefs(channelPref, channel, group.isBlocked()); + finalOrderedPrefs.add(channelPref); + } + int postAddPrefCount = groupPrefGroup.getPreferenceCount(); + + // If any channels were inserted (into a non-empty list) or need to be removed, we need to + // remove all preferences and re-add them all. + // This is required to ensure proper ordering of inserted channels, and it simplifies logic + // at the cost of computation in the rare case that the list is changing. + int numFinalGroups = finalOrderedPrefs.size(); + boolean hasInsertions = !initiallyEmpty && initialPrefCount != numFinalGroups; + boolean requiresRemoval = postAddPrefCount != numFinalGroups; + if (hasInsertions || requiresRemoval) { + groupPrefGroup.removeAll(); + for (Preference preference : finalOrderedPrefs) { + groupPrefGroup.addPreference(preference); + } + } + } + + /** Add or find and update the toggle for disabling the entire notification channel group. */ + private Preference addOrUpdateGroupToggle(@NonNull final PreferenceGroup parent, + @NonNull final NotificationChannelGroup group) { + boolean shouldAdd = false; + final RestrictedSwitchPreference preference; + if (parent.getPreferenceCount() > 0 + && parent.getPreference(0) instanceof RestrictedSwitchPreference) { + preference = (RestrictedSwitchPreference) parent.getPreference(0); + } else { + shouldAdd = true; + preference = new RestrictedSwitchPreference(mContext); + } + preference.setOrder(-1); preference.setTitle(mContext.getString( R.string.notification_switch_label, group.getName())); preference.setEnabled(mAdmin == null @@ -171,23 +300,26 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr onGroupBlockStateChanged(group); return true; }); - - parent.addPreference(preference); + if (shouldAdd) { + parent.addPreference(preference); + } + return preference; } - protected Preference populateSingleChannelPrefs(PreferenceGroup parent, - final NotificationChannel channel, final boolean groupBlocked) { - MasterSwitchPreference channelPref = new MasterSwitchPreference(mContext); + /** Update the properties of the channel preference with the values from the channel object. */ + private void updateSingleChannelPrefs(@NonNull final MasterSwitchPreference channelPref, + @NonNull final NotificationChannel channel, + final boolean groupBlocked) { channelPref.setSwitchEnabled(mAdmin == null && isChannelBlockable(channel) && isChannelConfigurable(channel) && !groupBlocked); - channelPref.setIcon(null); if (channel.getImportance() > IMPORTANCE_LOW) { channelPref.setIcon(getAlertingIcon()); + } else { + channelPref.setIcon(null); } channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL); - channelPref.setKey(channel.getId()); channelPref.setTitle(channel.getName()); channelPref.setSummary(NotificationBackend.getSentSummary( mContext, mAppRow.sentByChannel.get(channel.getId()), false)); @@ -219,10 +351,6 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr return true; }); - if (parent.findPreference(channelPref.getKey()) == null) { - parent.addPreference(channelPref); - } - return channelPref; } private Drawable getAlertingIcon() { @@ -235,30 +363,9 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr if (group == null) { return; } - PreferenceGroup groupGroup = mPreference.findPreference(group.getId()); - - if (groupGroup != null) { - if (group.isBlocked()) { - List toRemove = new ArrayList<>(); - int childCount = groupGroup.getPreferenceCount(); - for (int i = 0; i < childCount; i++) { - Preference pref = groupGroup.getPreference(i); - if (pref instanceof MasterSwitchPreference) { - toRemove.add(pref); - } - } - for (Preference pref : toRemove) { - groupGroup.removePreference(pref); - } - } else { - final List channels = group.getChannels(); - Collections.sort(channels, CHANNEL_COMPARATOR); - int N = channels.size(); - for (int i = 0; i < N; i++) { - final NotificationChannel channel = channels.get(i); - populateSingleChannelPrefs(groupGroup, channel, group.isBlocked()); - } - } + PreferenceGroup groupPrefGroup = mPreference.findPreference(group.getId()); + if (groupPrefGroup != null) { + updateGroupPreferences(group, groupPrefGroup); } } } diff --git a/src/com/android/settings/widget/MasterSwitchPreference.java b/src/com/android/settings/widget/MasterSwitchPreference.java index 9fe077e4496..7221035bb25 100644 --- a/src/com/android/settings/widget/MasterSwitchPreference.java +++ b/src/com/android/settings/widget/MasterSwitchPreference.java @@ -23,6 +23,8 @@ import android.view.View; import android.view.View.OnClickListener; import android.widget.Switch; +import androidx.annotation.Keep; +import androidx.annotation.Nullable; import androidx.preference.PreferenceViewHolder; import com.android.settings.R; @@ -101,6 +103,16 @@ public class MasterSwitchPreference extends RestrictedPreference { return mSwitch != null && mChecked; } + /** + * Used to validate the state of mChecked and mCheckedSet when testing, without requiring + * that a ViewHolder be bound to the object. + */ + @Keep + @Nullable + public Boolean getCheckedState() { + return mCheckedSet ? mChecked : null; + } + public void setChecked(boolean checked) { // Always set checked the first time; don't assume the field's default of false. final boolean changed = mChecked != checked; diff --git a/tests/unit/src/com/android/settings/notification/app/ChannelListPreferenceControllerTest.java b/tests/unit/src/com/android/settings/notification/app/ChannelListPreferenceControllerTest.java new file mode 100644 index 00000000000..f9c81321af5 --- /dev/null +++ b/tests/unit/src/com/android/settings/notification/app/ChannelListPreferenceControllerTest.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2020 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.app; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; +import static android.app.NotificationManager.IMPORTANCE_HIGH; +import static android.app.NotificationManager.IMPORTANCE_NONE; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertFalse; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +import android.app.Instrumentation; +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.content.Context; + +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceGroup; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; +import androidx.test.annotation.UiThreadTest; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import com.android.settings.notification.NotificationBackend; +import com.android.settings.notification.NotificationBackend.NotificationsSentState; +import com.android.settings.widget.MasterSwitchPreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ChannelListPreferenceControllerTest { + private Context mContext; + private NotificationBackend mBackend; + private NotificationBackend.AppRow mAppRow; + private ChannelListPreferenceController mController; + private PreferenceManager mPreferenceManager; + private PreferenceScreen mPreferenceScreen; + private PreferenceCategory mGroupList; + + @Before + public void setUp() throws Exception { + Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + mContext = instrumentation.getTargetContext(); + + instrumentation.runOnMainSync(() -> { + mBackend = new NotificationBackend(); + mAppRow = mBackend.loadAppRow(mContext, + mContext.getPackageManager(), mContext.getApplicationInfo()); + mController = new ChannelListPreferenceController(mContext, mBackend); + mController.onResume(mAppRow, null, null, null, null, null); + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + mGroupList = new PreferenceCategory(mContext); + mPreferenceScreen.addPreference(mGroupList); + }); + } + + @Test + @UiThreadTest + public void testUpdateFullList_incrementalUpdates() { + // Start by testing the case with no groups or channels + List inGroups = new ArrayList<>(); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + assertEquals("zeroCategories", mGroupList.getPreference(0).getKey()); + } + + // Test that adding a group clears the zero category and adds everything + NotificationChannelGroup inGroup1 = new NotificationChannelGroup("group1", "Group 1"); + inGroup1.addChannel(new NotificationChannel("ch1a", "Channel 1A", IMPORTANCE_DEFAULT)); + inGroups.add(inGroup1); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group1", group1.getKey()); + assertEquals(2, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + } + + // Test that adding a channel works -- no dupes or omissions + inGroup1.addChannel(new NotificationChannel("ch1b", "Channel 1B", IMPORTANCE_DEFAULT)); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B", group1.getPreference(2).getTitle()); + } + + // Test that renaming a channel does in fact rename the preferences + inGroup1.getChannels().get(1).setName("Channel 1B - Renamed"); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B - Renamed", group1.getPreference(2).getTitle()); + } + + // Test that adding a group works and results in the correct sorting. + NotificationChannelGroup inGroup0 = new NotificationChannelGroup("group0", "Group 0"); + inGroup0.addChannel(new NotificationChannel("ch0b", "Channel 0B", IMPORTANCE_DEFAULT)); + // NOTE: updateFullList takes a List which has been sorted, so we insert at 0 for this check + inGroups.add(0, inGroup0); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(2, mGroupList.getPreferenceCount()); + PreferenceGroup group0 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group0", group0.getKey()); + assertEquals(2, group0.getPreferenceCount()); + assertNull(group0.getPreference(0).getKey()); + assertEquals("All \"Group 0\" notifications", group0.getPreference(0).getTitle()); + assertEquals("ch0b", group0.getPreference(1).getKey()); + assertEquals("Channel 0B", group0.getPreference(1).getTitle()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(1); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B - Renamed", group1.getPreference(2).getTitle()); + } + + // Test that adding a channel that comes before another works and has correct ordering. + // NOTE: the channels within a group are sorted inside updateFullList. + inGroup0.addChannel(new NotificationChannel("ch0a", "Channel 0A", IMPORTANCE_DEFAULT)); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(2, mGroupList.getPreferenceCount()); + PreferenceGroup group0 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group0", group0.getKey()); + assertEquals(3, group0.getPreferenceCount()); + assertNull(group0.getPreference(0).getKey()); + assertEquals("All \"Group 0\" notifications", group0.getPreference(0).getTitle()); + assertEquals("ch0a", group0.getPreference(1).getKey()); + assertEquals("Channel 0A", group0.getPreference(1).getTitle()); + assertEquals("ch0b", group0.getPreference(2).getKey()); + assertEquals("Channel 0B", group0.getPreference(2).getTitle()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(1); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B - Renamed", group1.getPreference(2).getTitle()); + } + + // Test that the "Other" group works. + // Also test a simultaneous addition and deletion. + inGroups.remove(inGroup0); + NotificationChannelGroup inGroupOther = new NotificationChannelGroup(null, null); + inGroupOther.addChannel(new NotificationChannel("chXa", "Other A", IMPORTANCE_DEFAULT)); + inGroupOther.addChannel(new NotificationChannel("chXb", "Other B", IMPORTANCE_DEFAULT)); + inGroups.add(inGroupOther); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(2, mGroupList.getPreferenceCount()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B - Renamed", group1.getPreference(2).getTitle()); + PreferenceGroup groupOther = (PreferenceGroup) mGroupList.getPreference(1); + assertEquals("categories", groupOther.getKey()); + assertEquals(2, groupOther.getPreferenceCount()); + assertEquals("chXa", groupOther.getPreference(0).getKey()); + assertEquals("Other A", groupOther.getPreference(0).getTitle()); + assertEquals("chXb", groupOther.getPreference(1).getKey()); + assertEquals("Other B", groupOther.getPreference(1).getTitle()); + } + + // Test that the removal of a channel works. + inGroupOther.getChannels().remove(0); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(2, mGroupList.getPreferenceCount()); + PreferenceGroup group1 = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group1", group1.getKey()); + assertEquals(3, group1.getPreferenceCount()); + assertNull(group1.getPreference(0).getKey()); + assertEquals("All \"Group 1\" notifications", group1.getPreference(0).getTitle()); + assertEquals("ch1a", group1.getPreference(1).getKey()); + assertEquals("Channel 1A", group1.getPreference(1).getTitle()); + assertEquals("ch1b", group1.getPreference(2).getKey()); + assertEquals("Channel 1B - Renamed", group1.getPreference(2).getTitle()); + PreferenceGroup groupOther = (PreferenceGroup) mGroupList.getPreference(1); + assertEquals("categories", groupOther.getKey()); + assertEquals(1, groupOther.getPreferenceCount()); + assertEquals("chXb", groupOther.getPreference(0).getKey()); + assertEquals("Other B", groupOther.getPreference(0).getTitle()); + } + + // Test that we go back to the empty state when clearing all groups and channels. + inGroups.clear(); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + assertEquals("zeroCategories", mGroupList.getPreference(0).getKey()); + } + } + + + @Test + @UiThreadTest + public void testUpdateFullList_groupBlockedChange() { + List inGroups = new ArrayList<>(); + NotificationChannelGroup inGroup = new NotificationChannelGroup("group", "My Group"); + inGroup.addChannel(new NotificationChannel("channelA", "Channel A", IMPORTANCE_DEFAULT)); + inGroup.addChannel(new NotificationChannel("channelB", "Channel B", IMPORTANCE_NONE)); + inGroups.add(inGroup); + + // Test that the group is initially showing all preferences + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group", group.getKey()); + assertEquals(3, group.getPreferenceCount()); + SwitchPreference groupBlockPref = (SwitchPreference) group.getPreference(0); + assertNull(groupBlockPref.getKey()); + assertEquals("All \"My Group\" notifications", groupBlockPref.getTitle()); + assertTrue(groupBlockPref.isChecked()); + MasterSwitchPreference channelAPref = (MasterSwitchPreference) group.getPreference(1); + assertEquals("channelA", channelAPref.getKey()); + assertEquals("Channel A", channelAPref.getTitle()); + assertEquals(Boolean.TRUE, channelAPref.getCheckedState()); + MasterSwitchPreference channelBPref = (MasterSwitchPreference) group.getPreference(2); + assertEquals("channelB", channelBPref.getKey()); + assertEquals("Channel B", channelBPref.getTitle()); + assertEquals(Boolean.FALSE, channelBPref.getCheckedState()); + } + + // Test that when a group is blocked, the list removes its individual channel preferences + inGroup.setBlocked(true); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group", group.getKey()); + assertEquals(1, group.getPreferenceCount()); + SwitchPreference groupBlockPref = (SwitchPreference) group.getPreference(0); + assertNull(groupBlockPref.getKey()); + assertEquals("All \"My Group\" notifications", groupBlockPref.getTitle()); + assertFalse(groupBlockPref.isChecked()); + } + + // Test that when a group is unblocked, the list adds its individual channel preferences + inGroup.setBlocked(false); + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group", group.getKey()); + assertEquals(3, group.getPreferenceCount()); + SwitchPreference groupBlockPref = (SwitchPreference) group.getPreference(0); + assertNull(groupBlockPref.getKey()); + assertEquals("All \"My Group\" notifications", groupBlockPref.getTitle()); + assertTrue(groupBlockPref.isChecked()); + MasterSwitchPreference channelAPref = (MasterSwitchPreference) group.getPreference(1); + assertEquals("channelA", channelAPref.getKey()); + assertEquals("Channel A", channelAPref.getTitle()); + assertEquals(Boolean.TRUE, channelAPref.getCheckedState()); + MasterSwitchPreference channelBPref = (MasterSwitchPreference) group.getPreference(2); + assertEquals("channelB", channelBPref.getKey()); + assertEquals("Channel B", channelBPref.getTitle()); + assertEquals(Boolean.FALSE, channelBPref.getCheckedState()); + } + } + + @Test + @UiThreadTest + public void testUpdateFullList_channelUpdates() { + List inGroups = new ArrayList<>(); + NotificationChannelGroup inGroup = new NotificationChannelGroup("group", "Group"); + NotificationChannel channelA = + new NotificationChannel("channelA", "Channel A", IMPORTANCE_HIGH); + NotificationChannel channelB = + new NotificationChannel("channelB", "Channel B", IMPORTANCE_NONE); + inGroup.addChannel(channelA); + inGroup.addChannel(channelB); + inGroups.add(inGroup); + + NotificationsSentState sentA = new NotificationsSentState(); + sentA.avgSentDaily = 2; + sentA.avgSentWeekly = 10; + NotificationsSentState sentB = new NotificationsSentState(); + sentB.avgSentDaily = 0; + sentB.avgSentWeekly = 2; + mAppRow.sentByChannel.put("channelA", sentA); + + // Test that the channels' properties are reflected in the preference + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group", group.getKey()); + assertEquals(3, group.getPreferenceCount()); + assertNull(group.getPreference(0).getKey()); + assertEquals("All \"Group\" notifications", group.getPreference(0).getTitle()); + MasterSwitchPreference channelAPref = (MasterSwitchPreference) group.getPreference(1); + assertEquals("channelA", channelAPref.getKey()); + assertEquals("Channel A", channelAPref.getTitle()); + assertEquals(Boolean.TRUE, channelAPref.getCheckedState()); + assertEquals("~2 notifications per day", channelAPref.getSummary()); + assertNotNull(channelAPref.getIcon()); + MasterSwitchPreference channelBPref = (MasterSwitchPreference) group.getPreference(2); + assertEquals("channelB", channelBPref.getKey()); + assertEquals("Channel B", channelBPref.getTitle()); + assertEquals(Boolean.FALSE, channelBPref.getCheckedState()); + assertNull(channelBPref.getSummary()); + assertNull(channelBPref.getIcon()); + } + + channelA.setImportance(IMPORTANCE_NONE); + channelB.setImportance(IMPORTANCE_DEFAULT); + + mAppRow.sentByChannel.remove("channelA"); + mAppRow.sentByChannel.put("channelB", sentB); + + // Test that changing the channels' properties correctly updates the preference + mController.updateFullList(mGroupList, inGroups); + { + assertEquals(1, mGroupList.getPreferenceCount()); + PreferenceGroup group = (PreferenceGroup) mGroupList.getPreference(0); + assertEquals("group", group.getKey()); + assertEquals(3, group.getPreferenceCount()); + assertNull(group.getPreference(0).getKey()); + assertEquals("All \"Group\" notifications", group.getPreference(0).getTitle()); + MasterSwitchPreference channelAPref = (MasterSwitchPreference) group.getPreference(1); + assertEquals("channelA", channelAPref.getKey()); + assertEquals("Channel A", channelAPref.getTitle()); + assertEquals(Boolean.FALSE, channelAPref.getCheckedState()); + assertNull(channelAPref.getSummary()); + assertNull(channelAPref.getIcon()); + MasterSwitchPreference channelBPref = (MasterSwitchPreference) group.getPreference(2); + assertEquals("channelB", channelBPref.getKey()); + assertEquals("Channel B", channelBPref.getTitle()); + assertEquals(Boolean.TRUE, channelBPref.getCheckedState()); + assertEquals("~2 notifications per week", channelBPref.getSummary()); + assertNotNull(channelBPref.getIcon()); + } + } + +}