Update NotificationChannelSettings Preferences in place to prevent re-layout.
(cherry picked from commit b409b64045
)
Fixes: 110093185
Test: ChannelListPreferenceControllerTest
Change-Id: If6acf305c44085e502a3304ea57e409ce049b40f
Merged-In: If6acf305c44085e502a3304ea57e409ce049b40f
This commit is contained in:
@@ -23,16 +23,14 @@ import android.app.NotificationChannel;
|
|||||||
import android.app.NotificationChannelGroup;
|
import android.app.NotificationChannelGroup;
|
||||||
import android.app.settings.SettingsEnums;
|
import android.app.settings.SettingsEnums;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.BlendMode;
|
|
||||||
import android.graphics.BlendModeColorFilter;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.graphics.drawable.LayerDrawable;
|
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.preference.Preference;
|
import androidx.preference.Preference;
|
||||||
import androidx.preference.PreferenceCategory;
|
import androidx.preference.PreferenceCategory;
|
||||||
import androidx.preference.PreferenceGroup;
|
import androidx.preference.PreferenceGroup;
|
||||||
@@ -53,7 +51,8 @@ import java.util.List;
|
|||||||
public class ChannelListPreferenceController extends NotificationPreferenceController {
|
public class ChannelListPreferenceController extends NotificationPreferenceController {
|
||||||
|
|
||||||
private static final String KEY = "channels";
|
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";
|
public static final String ARG_FROM_SETTINGS = "fromSettings";
|
||||||
|
|
||||||
private List<NotificationChannelGroup> mChannelGroupList;
|
private List<NotificationChannelGroup> mChannelGroupList;
|
||||||
@@ -102,62 +101,192 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr
|
|||||||
if (mContext == null) {
|
if (mContext == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
populateList();
|
updateFullList(mPreference, mChannelGroupList);
|
||||||
}
|
}
|
||||||
}.execute();
|
}.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateList() {
|
/**
|
||||||
// TODO: if preference has children, compare with newly loaded list
|
* Update the preferences group to match the
|
||||||
mPreference.removeAll();
|
* @param groupPrefsList
|
||||||
|
* @param channelGroups
|
||||||
if (mChannelGroupList.isEmpty()) {
|
*/
|
||||||
PreferenceCategory groupCategory = new PreferenceCategory(mContext);
|
void updateFullList(@NonNull PreferenceCategory groupPrefsList,
|
||||||
groupCategory.setTitle(R.string.notification_channels);
|
@NonNull List<NotificationChannelGroup> channelGroups) {
|
||||||
groupCategory.setKey(KEY_GENERAL_CATEGORY);
|
if (channelGroups.isEmpty()) {
|
||||||
mPreference.addPreference(groupCategory);
|
if (groupPrefsList.getPreferenceCount() == 1
|
||||||
|
&& KEY_ZERO_CATEGORIES.equals(groupPrefsList.getPreference(0).getKey())) {
|
||||||
Preference empty = new Preference(mContext);
|
// Ensure the titles are correct for the current language, but otherwise leave alone
|
||||||
empty.setTitle(R.string.no_channels);
|
PreferenceGroup groupCategory = (PreferenceGroup) groupPrefsList.getPreference(0);
|
||||||
empty.setEnabled(false);
|
groupCategory.setTitle(R.string.notification_channels);
|
||||||
groupCategory.addPreference(empty);
|
groupCategory.getPreference(0).setTitle(R.string.no_channels);
|
||||||
} 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);
|
|
||||||
} else {
|
} else {
|
||||||
groupCategory.setTitle(group.getName());
|
// Clear any contents and create the 'zero-categories' group.
|
||||||
groupCategory.setKey(group.getId());
|
groupPrefsList.removeAll();
|
||||||
populateGroupToggle(groupCategory, group);
|
|
||||||
|
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()) {
|
} else {
|
||||||
final List<NotificationChannel> channels = group.getChannels();
|
updateGroupList(groupPrefsList, channelGroups);
|
||||||
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
|
* Looks for the category for the given group's key at the expected index, if that doesn't
|
||||||
if (TextUtils.isEmpty(channel.getConversationId()) || channel.isDemoted()) {
|
* match, it checks all groups, and if it can't find that group anywhere, it creates it.
|
||||||
populateSingleChannelPrefs(groupCategory, channel, group.isBlocked());
|
*/
|
||||||
}
|
@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<NotificationChannelGroup> 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<PreferenceCategory> 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) {
|
* Looks for the channel preference for the given channel's key at the expected index, if that
|
||||||
RestrictedSwitchPreference preference =
|
* doesn't match, it checks all rows, and if it can't find that channel anywhere, it creates
|
||||||
new RestrictedSwitchPreference(mContext);
|
* 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<Preference> 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<NotificationChannel> 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(
|
preference.setTitle(mContext.getString(
|
||||||
R.string.notification_switch_label, group.getName()));
|
R.string.notification_switch_label, group.getName()));
|
||||||
preference.setEnabled(mAdmin == null
|
preference.setEnabled(mAdmin == null
|
||||||
@@ -171,23 +300,26 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr
|
|||||||
onGroupBlockStateChanged(group);
|
onGroupBlockStateChanged(group);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
if (shouldAdd) {
|
||||||
parent.addPreference(preference);
|
parent.addPreference(preference);
|
||||||
|
}
|
||||||
|
return preference;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Preference populateSingleChannelPrefs(PreferenceGroup parent,
|
/** Update the properties of the channel preference with the values from the channel object. */
|
||||||
final NotificationChannel channel, final boolean groupBlocked) {
|
private void updateSingleChannelPrefs(@NonNull final MasterSwitchPreference channelPref,
|
||||||
MasterSwitchPreference channelPref = new MasterSwitchPreference(mContext);
|
@NonNull final NotificationChannel channel,
|
||||||
|
final boolean groupBlocked) {
|
||||||
channelPref.setSwitchEnabled(mAdmin == null
|
channelPref.setSwitchEnabled(mAdmin == null
|
||||||
&& isChannelBlockable(channel)
|
&& isChannelBlockable(channel)
|
||||||
&& isChannelConfigurable(channel)
|
&& isChannelConfigurable(channel)
|
||||||
&& !groupBlocked);
|
&& !groupBlocked);
|
||||||
channelPref.setIcon(null);
|
|
||||||
if (channel.getImportance() > IMPORTANCE_LOW) {
|
if (channel.getImportance() > IMPORTANCE_LOW) {
|
||||||
channelPref.setIcon(getAlertingIcon());
|
channelPref.setIcon(getAlertingIcon());
|
||||||
|
} else {
|
||||||
|
channelPref.setIcon(null);
|
||||||
}
|
}
|
||||||
channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL);
|
channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL);
|
||||||
channelPref.setKey(channel.getId());
|
|
||||||
channelPref.setTitle(channel.getName());
|
channelPref.setTitle(channel.getName());
|
||||||
channelPref.setSummary(NotificationBackend.getSentSummary(
|
channelPref.setSummary(NotificationBackend.getSentSummary(
|
||||||
mContext, mAppRow.sentByChannel.get(channel.getId()), false));
|
mContext, mAppRow.sentByChannel.get(channel.getId()), false));
|
||||||
@@ -219,10 +351,6 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (parent.findPreference(channelPref.getKey()) == null) {
|
|
||||||
parent.addPreference(channelPref);
|
|
||||||
}
|
|
||||||
return channelPref;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Drawable getAlertingIcon() {
|
private Drawable getAlertingIcon() {
|
||||||
@@ -235,30 +363,9 @@ public class ChannelListPreferenceController extends NotificationPreferenceContr
|
|||||||
if (group == null) {
|
if (group == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
PreferenceGroup groupGroup = mPreference.findPreference(group.getId());
|
PreferenceGroup groupPrefGroup = mPreference.findPreference(group.getId());
|
||||||
|
if (groupPrefGroup != null) {
|
||||||
if (groupGroup != null) {
|
updateGroupPreferences(group, groupPrefGroup);
|
||||||
if (group.isBlocked()) {
|
|
||||||
List<Preference> 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<NotificationChannel> 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,6 +23,8 @@ import android.view.View;
|
|||||||
import android.view.View.OnClickListener;
|
import android.view.View.OnClickListener;
|
||||||
import android.widget.Switch;
|
import android.widget.Switch;
|
||||||
|
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.preference.PreferenceViewHolder;
|
import androidx.preference.PreferenceViewHolder;
|
||||||
|
|
||||||
import com.android.settings.R;
|
import com.android.settings.R;
|
||||||
@@ -101,6 +103,16 @@ public class MasterSwitchPreference extends RestrictedPreference {
|
|||||||
return mSwitch != null && mChecked;
|
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) {
|
public void setChecked(boolean checked) {
|
||||||
// Always set checked the first time; don't assume the field's default of false.
|
// Always set checked the first time; don't assume the field's default of false.
|
||||||
final boolean changed = mChecked != checked;
|
final boolean changed = mChecked != checked;
|
||||||
|
@@ -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<NotificationChannelGroup> 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<NotificationChannelGroup> 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<NotificationChannelGroup> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user