From 00d824657dc577eb3d3779382672afc643122a5a Mon Sep 17 00:00:00 2001 From: Yuri Lin Date: Mon, 12 Jul 2021 12:02:53 -0400 Subject: [PATCH] Merge messages & conversations settings for DND priority senders. This change explicitly allows for multiselect between priority senders (starred, contacts) & priority conversations, and also allows unchecking boxes by clicking on the same ones again. Also makes the screens for setting messages and calls in custom rules consistent in behavior with the main DND settings. Since much of the functionality is shared, this change refactors most of the logic into a new helper class used by both. While these changes also affect how the "calls" screen is constructed, in effect there is no change to the functionality of priority call sender settings except under the hood. Test: atest ZenPrioritySendersHelperTest; Settings robotests Bug: 190180868 Bug: 197223270 Change-Id: I894775537a18feb7a891b2668b9a613a203a129c --- res/values/strings.xml | 4 + res/xml/zen_mode_calls_settings.xml | 6 - ...zen_mode_custom_rule_messages_settings.xml | 11 +- res/xml/zen_mode_messages_settings.xml | 6 - res/xml/zen_mode_people_settings.xml | 26 +- .../zen/ZenCustomRuleMessagesSettings.java | 13 +- .../notification/zen/ZenModeBackend.java | 14 + .../zen/ZenModeCallsSettings.java | 6 +- .../zen/ZenModeMessagesSettings.java | 6 +- .../zen/ZenModePeopleSettings.java | 4 - ...dePrioritySendersPreferenceController.java | 204 ++----- .../notification/zen/ZenModeSettings.java | 33 +- .../zen/ZenPrioritySendersHelper.java | 388 ++++++++++++ ...lePrioritySendersPreferenceController.java | 178 ++++++ ...ioritySendersPreferenceControllerTest.java | 191 +++--- ...ioritySendersPreferenceControllerTest.java | 238 ++++++++ .../zen/ZenPrioritySendersHelperTest.java | 559 ++++++++++++++++++ 17 files changed, 1585 insertions(+), 302 deletions(-) create mode 100644 src/com/android/settings/notification/zen/ZenPrioritySendersHelper.java create mode 100644 src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceControllerTest.java create mode 100644 tests/unit/src/com/android/settings/notification/zen/ZenPrioritySendersHelperTest.java diff --git a/res/values/strings.xml b/res/values/strings.xml index 11a9bf4c347..833eb48f120 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9801,6 +9801,8 @@ Conversations that can interrupt All conversations Priority conversations + + priority conversations None @@ -9874,6 +9876,8 @@ Contacts Starred contacts + + Some people or conversations From starred contacts and repeat callers diff --git a/res/xml/zen_mode_calls_settings.xml b/res/xml/zen_mode_calls_settings.xml index acd802334c3..a0b39a9c525 100644 --- a/res/xml/zen_mode_calls_settings.xml +++ b/res/xml/zen_mode_calls_settings.xml @@ -26,12 +26,6 @@ android:key="zen_mode_settings_category_calls" android:title="@string/zen_mode_calls_header" settings:allowDividerBelow="true"> - - - diff --git a/res/xml/zen_mode_custom_rule_messages_settings.xml b/res/xml/zen_mode_custom_rule_messages_settings.xml index 66091ec7e3e..aff903651b3 100644 --- a/res/xml/zen_mode_custom_rule_messages_settings.xml +++ b/res/xml/zen_mode_custom_rule_messages_settings.xml @@ -23,16 +23,7 @@ - - - - + diff --git a/res/xml/zen_mode_messages_settings.xml b/res/xml/zen_mode_messages_settings.xml index 797650f7057..6f537aa74d4 100644 --- a/res/xml/zen_mode_messages_settings.xml +++ b/res/xml/zen_mode_messages_settings.xml @@ -25,12 +25,6 @@ - - - diff --git a/res/xml/zen_mode_people_settings.xml b/res/xml/zen_mode_people_settings.xml index 1db438871be..ed21435018a 100644 --- a/res/xml/zen_mode_people_settings.xml +++ b/res/xml/zen_mode_people_settings.xml @@ -20,37 +20,21 @@ xmlns:settings="http://schemas.android.com/apk/res-auto" android:title="@string/zen_category_people" > - - - - - - - - - + + - diff --git a/src/com/android/settings/notification/zen/ZenCustomRuleMessagesSettings.java b/src/com/android/settings/notification/zen/ZenCustomRuleMessagesSettings.java index d4d37300afb..7b2bd4adcab 100644 --- a/src/com/android/settings/notification/zen/ZenCustomRuleMessagesSettings.java +++ b/src/com/android/settings/notification/zen/ZenCustomRuleMessagesSettings.java @@ -18,12 +18,12 @@ package com.android.settings.notification.zen; import android.app.settings.SettingsEnums; import android.content.Context; -import android.service.notification.ZenPolicy; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.widget.FooterPreference; @@ -32,7 +32,6 @@ import java.util.List; public class ZenCustomRuleMessagesSettings extends ZenCustomRuleSettingsBase { private static final String MESSAGES_KEY = "zen_mode_messages"; - private static final String STARRED_CONTACTS_KEY = "zen_mode_starred_contacts_messages"; private static final String PREFERENCE_CATEGORY_KEY = "zen_mode_settings_category_messages"; @Override @@ -48,11 +47,9 @@ public class ZenCustomRuleMessagesSettings extends ZenCustomRuleSettingsBase { @Override protected List createPreferenceControllers(Context context) { mControllers = new ArrayList<>(); - mControllers.add(new ZenRuleMessagesPreferenceController(context, MESSAGES_KEY, - getSettingsLifecycle())); - mControllers.add(new ZenRuleStarredContactsPreferenceController(context, - getSettingsLifecycle(), ZenPolicy.PRIORITY_CATEGORY_MESSAGES, - STARRED_CONTACTS_KEY)); + mControllers.add(new ZenRulePrioritySendersPreferenceController(context, + PREFERENCE_CATEGORY_KEY, getSettingsLifecycle(), true, + new NotificationBackend())); return mControllers; } @@ -65,6 +62,8 @@ public class ZenCustomRuleMessagesSettings extends ZenCustomRuleSettingsBase { public void updatePreferences() { super.updatePreferences(); PreferenceScreen screen = getPreferenceScreen(); + // TODO(b/200600958): It seems that this string does not currently update to indicate when + // messages aren't in fact blocked by the rule. Preference footerPreference = screen.findPreference(FooterPreference.KEY_FOOTER); footerPreference.setTitle(mContext.getResources().getString( R.string.zen_mode_custom_messages_footer, mRule.getName())); diff --git a/src/com/android/settings/notification/zen/ZenModeBackend.java b/src/com/android/settings/notification/zen/ZenModeBackend.java index e3f5063bf23..85f9aeea5fe 100644 --- a/src/com/android/settings/notification/zen/ZenModeBackend.java +++ b/src/com/android/settings/notification/zen/ZenModeBackend.java @@ -287,6 +287,20 @@ public class ZenModeBackend { } } + protected static int getContactSettingFromZenPolicySetting(int setting) { + switch (setting) { + case ZenPolicy.PEOPLE_TYPE_ANYONE: + return NotificationManager.Policy.PRIORITY_SENDERS_ANY; + case ZenPolicy.PEOPLE_TYPE_CONTACTS: + return NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; + case ZenPolicy.PEOPLE_TYPE_STARRED: + return NotificationManager.Policy.PRIORITY_SENDERS_STARRED; + case ZenPolicy.PEOPLE_TYPE_NONE: + default: + return SOURCE_NONE; + } + } + protected int getAlarmsTotalSilencePeopleSummary(int category) { if (category == NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES) { return R.string.zen_mode_none_messages; diff --git a/src/com/android/settings/notification/zen/ZenModeCallsSettings.java b/src/com/android/settings/notification/zen/ZenModeCallsSettings.java index 82d5cf62765..93e97b1d2cd 100644 --- a/src/com/android/settings/notification/zen/ZenModeCallsSettings.java +++ b/src/com/android/settings/notification/zen/ZenModeCallsSettings.java @@ -21,6 +21,7 @@ import android.content.Context; import android.provider.SearchIndexableResource; import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -44,9 +45,8 @@ public class ZenModeCallsSettings extends ZenModeSettingsBase { Lifecycle lifecycle) { List controllers = new ArrayList<>(); controllers.add(new ZenModePrioritySendersPreferenceController(context, - "zen_mode_settings_category_calls", lifecycle, false)); - controllers.add(new ZenModeSendersImagePreferenceController(context, - "zen_mode_calls_image", lifecycle, false)); + "zen_mode_settings_category_calls", lifecycle, false, + new NotificationBackend())); controllers.add(new ZenModeRepeatCallersPreferenceController(context, lifecycle, context.getResources().getInteger(com.android.internal.R.integer .config_zen_repeat_callers_threshold))); diff --git a/src/com/android/settings/notification/zen/ZenModeMessagesSettings.java b/src/com/android/settings/notification/zen/ZenModeMessagesSettings.java index f8e4548f53f..3405c435ba3 100644 --- a/src/com/android/settings/notification/zen/ZenModeMessagesSettings.java +++ b/src/com/android/settings/notification/zen/ZenModeMessagesSettings.java @@ -21,6 +21,7 @@ import android.content.Context; import android.provider.SearchIndexableResource; import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settings.search.BaseSearchIndexProvider; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -44,10 +45,9 @@ public class ZenModeMessagesSettings extends ZenModeSettingsBase { private static List buildPreferenceControllers(Context context, Lifecycle lifecycle) { List controllers = new ArrayList<>(); - controllers.add(new ZenModeSendersImagePreferenceController(context, - "zen_mode_messages_image", lifecycle, true)); controllers.add(new ZenModePrioritySendersPreferenceController(context, - "zen_mode_settings_category_messages", lifecycle, true)); + "zen_mode_settings_category_messages", lifecycle, true, + new NotificationBackend())); controllers.add(new ZenModeBehaviorFooterPreferenceController( context, lifecycle, R.string.zen_mode_messages_footer)); return controllers; diff --git a/src/com/android/settings/notification/zen/ZenModePeopleSettings.java b/src/com/android/settings/notification/zen/ZenModePeopleSettings.java index 962badbd05e..02d2647e2ef 100644 --- a/src/com/android/settings/notification/zen/ZenModePeopleSettings.java +++ b/src/com/android/settings/notification/zen/ZenModePeopleSettings.java @@ -56,10 +56,6 @@ public class ZenModePeopleSettings extends ZenModeSettingsBase implements Indexa Lifecycle lifecycle, Application app, Fragment host, FragmentManager fragmentManager, NotificationBackend notificationBackend) { List controllers = new ArrayList<>(); - controllers.add(new ZenModeConversationsImagePreferenceController(context, - "zen_mode_conversations_image", lifecycle, notificationBackend)); - controllers.add(new ZenModeConversationsPreferenceController(context, - "zen_mode_conversations", lifecycle)); controllers.add(new ZenModeCallsPreferenceController(context, lifecycle, "zen_mode_people_calls")); controllers.add(new ZenModeMessagesPreferenceController(context, lifecycle, diff --git a/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceController.java index 8bad60b9968..0d6093c040d 100644 --- a/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceController.java +++ b/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceController.java @@ -19,83 +19,53 @@ package com.android.settings.notification.zen; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES; -import android.app.NotificationManager; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.UNKNOWN; + import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.provider.Contacts; -import android.view.View; +import android.os.AsyncTask; import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; -import com.android.settings.R; +import com.android.settings.notification.NotificationBackend; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.widget.SelectorWithWidgetPreference; -import java.util.ArrayList; -import java.util.List; - /** - * Common preference controller functionality shared by - * ZenModePriorityMessagesPreferenceController and ZenModePriorityCallsPreferenceController. + * Common preference controller functionality for zen mode priority senders preferences for both + * messages and calls. * - * This includes the options to choose the priority senders that are allowed to bypass DND for - * calls or messages. This can be one of four values: starred contacts, all contacts, anyone, or - * no one. + * These controllers handle the settings regarding which priority senders that are allowed to + * bypass DND for calls or messages, which may be one the following values: starred contacts, all + * contacts, priority conversations (for messages only), anyone, or no one. + * + * Most of the functionality is handled by ZenPrioritySendersHelper, so that it can also be shared + * with settings controllers for custom rules. This class handles the parts of the behavior where + * settings must be written to the relevant backends, as that's where this class diverges from + * custom rules. */ public class ZenModePrioritySendersPreferenceController extends AbstractZenModePreferenceController { - @VisibleForTesting static final String KEY_ANY = "senders_anyone"; - @VisibleForTesting static final String KEY_CONTACTS = "senders_contacts"; - @VisibleForTesting static final String KEY_STARRED = "senders_starred_contacts"; - @VisibleForTesting static final String KEY_NONE = "senders_none"; - - private static final Intent ALL_CONTACTS_INTENT = - new Intent(Contacts.Intents.UI.LIST_DEFAULT) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - private static final Intent STARRED_CONTACTS_INTENT = - new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); - - private final PackageManager mPackageManager; private final boolean mIsMessages; // if this is false, then this preference is for calls private PreferenceCategory mPreferenceCategory; - private List mSelectorWithWidgetPreferences = new ArrayList<>(); + private ZenPrioritySendersHelper mHelper; public ZenModePrioritySendersPreferenceController(Context context, String key, - Lifecycle lifecycle, boolean isMessages) { + Lifecycle lifecycle, boolean isMessages, NotificationBackend notificationBackend) { super(context, key, lifecycle); mIsMessages = isMessages; - mPackageManager = mContext.getPackageManager(); - if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { - FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); - } + mHelper = new ZenPrioritySendersHelper( + context, isMessages, mBackend, notificationBackend, mSelectorClickListener); } @Override public void displayPreference(PreferenceScreen screen) { mPreferenceCategory = screen.findPreference(getPreferenceKey()); - if (mPreferenceCategory.findPreference(KEY_ANY) == null) { - makeRadioPreference(KEY_STARRED, - com.android.settings.R.string.zen_mode_from_starred); - makeRadioPreference(KEY_CONTACTS, - com.android.settings.R.string.zen_mode_from_contacts); - makeRadioPreference(KEY_ANY, - com.android.settings.R.string.zen_mode_from_anyone); - makeRadioPreference(KEY_NONE, - mIsMessages - ? com.android.settings.R.string.zen_mode_none_messages - : com.android.settings.R.string.zen_mode_none_calls); - updateSummaries(); - } - + mHelper.displayPreference(mPreferenceCategory); super.displayPreference(screen); } @@ -111,53 +81,37 @@ public class ZenModePrioritySendersPreferenceController @Override public void updateState(Preference preference) { - final int currSetting = getPrioritySenders(); - - for (SelectorWithWidgetPreference pref : mSelectorWithWidgetPreferences) { - pref.setChecked(keyToSetting(pref.getKey()) == currSetting); - } + final int currContactsSetting = getPrioritySenders(); + final int currConversationsSetting = getPriorityConversationSenders(); + mHelper.updateState(currContactsSetting, currConversationsSetting); } @Override public void onResume() { super.onResume(); - updateSummaries(); - } - - private void updateSummaries() { - for (SelectorWithWidgetPreference pref : mSelectorWithWidgetPreferences) { - pref.setSummary(getSummary(pref.getKey())); + if (mIsMessages) { + updateChannelCounts(); } + mHelper.updateSummaries(); } - private static int keyToSetting(String key) { - switch (key) { - case KEY_STARRED: - return NotificationManager.Policy.PRIORITY_SENDERS_STARRED; - case KEY_CONTACTS: - return NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; - case KEY_ANY: - return NotificationManager.Policy.PRIORITY_SENDERS_ANY; - case KEY_NONE: - default: - return ZenModeBackend.SOURCE_NONE; - } - } - - private String getSummary(String key) { - switch (key) { - case KEY_STARRED: - return mBackend.getStarredContactsSummary(mContext); - case KEY_CONTACTS: - return mBackend.getContactsNumberSummary(mContext); - case KEY_ANY: - return mContext.getResources().getString(mIsMessages - ? R.string.zen_mode_all_messages_summary - : R.string.zen_mode_all_calls_summary); - case KEY_NONE: - default: + private void updateChannelCounts() { + // Load conversations + new AsyncTask() { + @Override + protected Void doInBackground(Void... unused) { + mHelper.updateChannelCounts(); return null; - } + } + + @Override + protected void onPostExecute(Void unused) { + if (mContext == null) { + return; + } + updateState(mPreferenceCategory); + } + }.execute(); } private int getPrioritySenders() { @@ -168,72 +122,34 @@ public class ZenModePrioritySendersPreferenceController } } - private SelectorWithWidgetPreference makeRadioPreference(String key, int titleId) { - final SelectorWithWidgetPreference pref = - new SelectorWithWidgetPreference(mPreferenceCategory.getContext()); - pref.setKey(key); - pref.setTitle(titleId); - pref.setOnClickListener(mRadioButtonClickListener); - - View.OnClickListener widgetClickListener = getWidgetClickListener(key); - if (widgetClickListener != null) { - pref.setExtraWidgetOnClickListener(widgetClickListener); + private int getPriorityConversationSenders() { + if (mIsMessages) { + return mBackend.getPriorityConversationSenders(); } - - mPreferenceCategory.addPreference(pref); - mSelectorWithWidgetPreferences.add(pref); - return pref; + return UNKNOWN; } - private SelectorWithWidgetPreference.OnClickListener mRadioButtonClickListener = + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = new SelectorWithWidgetPreference.OnClickListener() { @Override public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { - int selectedSetting = keyToSetting(preference.getKey()); - if (selectedSetting != getPrioritySenders()) { + // The settingsToSaveOnClick function takes whether or not the preference is a + // checkbox into account to determine whether this selection is checked or unchecked. + final int[] settingsToSave = mHelper.settingsToSaveOnClick(preference, + getPrioritySenders(), getPriorityConversationSenders()); + final int prioritySendersSetting = settingsToSave[0]; + final int priorityConvosSetting = settingsToSave[1]; + + if (prioritySendersSetting != UNKNOWN) { mBackend.saveSenders( mIsMessages ? PRIORITY_CATEGORY_MESSAGES : PRIORITY_CATEGORY_CALLS, - selectedSetting); + prioritySendersSetting); + } + + if (mIsMessages && priorityConvosSetting != UNKNOWN) { + mBackend.saveConversationSenders(priorityConvosSetting); } } }; - - private View.OnClickListener getWidgetClickListener(String key) { - if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key)) { - return null; - } - - if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { - return null; - } - - if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { - return null; - } - - return new View.OnClickListener() { - @Override - public void onClick(View v) { - if (KEY_STARRED.equals(key) - && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { - mContext.startActivity(STARRED_CONTACTS_INTENT); - } else if (KEY_CONTACTS.equals(key) - && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { - mContext.startActivity(ALL_CONTACTS_INTENT); - } else { - mContext.startActivity(FALLBACK_INTENT); - } - } - }; - } - - private boolean isStarredIntentValid() { - return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null - || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; - } - - private boolean isContactsIntentValid() { - return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null - || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; - } } diff --git a/src/com/android/settings/notification/zen/ZenModeSettings.java b/src/com/android/settings/notification/zen/ZenModeSettings.java index 2cc4f8b35e8..a707e534d74 100644 --- a/src/com/android/settings/notification/zen/ZenModeSettings.java +++ b/src/com/android/settings/notification/zen/ZenModeSettings.java @@ -18,6 +18,7 @@ package com.android.settings.notification.zen; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_ALARMS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; +import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_EVENTS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MEDIA; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES; @@ -116,6 +117,7 @@ public class ZenModeSettings extends ZenModeSettingsBase { PRIORITY_CATEGORY_MEDIA, PRIORITY_CATEGORY_SYSTEM, PRIORITY_CATEGORY_MESSAGES, + PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_CATEGORY_EVENTS, PRIORITY_CATEGORY_REMINDERS, PRIORITY_CATEGORY_CALLS, @@ -168,12 +170,19 @@ public class ZenModeSettings extends ZenModeSettingsBase { String getMessagesSettingSummary(Policy policy) { List enabledCategories = getEnabledCategories(policy, - category -> PRIORITY_CATEGORY_MESSAGES == category, false); + category -> PRIORITY_CATEGORY_MESSAGES == category + || PRIORITY_CATEGORY_CONVERSATIONS == category, true); int numCategories = enabledCategories.size(); if (numCategories == 0) { return mContext.getString(R.string.zen_mode_none_messages); - } else { + } else if (numCategories == 1) { return enabledCategories.get(0); + } else { + // While this string name seems like a slight misnomer: it's borrowing the analogous + // calls-summary functionality to combine two permissions. + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); } } @@ -250,6 +259,15 @@ public class ZenModeSettings extends ZenModeSettingsBase { continue; } + // For conversations, only the "priority conversations" setting is relevant; any + // other setting is subsumed by the messages-specific messaging. + if (category == Policy.PRIORITY_CATEGORY_CONVERSATIONS + && isCategoryEnabled(policy, Policy.PRIORITY_CATEGORY_CONVERSATIONS) + && policy.priorityConversationSenders + != Policy.CONVERSATION_SENDERS_IMPORTANT) { + continue; + } + enabledCategories.add(getCategory(category, policy, isFirst)); } } @@ -282,11 +300,20 @@ public class ZenModeSettings extends ZenModeSettingsBase { } else if (category == Policy.PRIORITY_CATEGORY_MESSAGES) { if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_ANY) { return mContext.getString(R.string.zen_mode_from_anyone); - } else if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_CONTACTS){ + } else if (policy.priorityMessageSenders == Policy.PRIORITY_SENDERS_CONTACTS) { return mContext.getString(R.string.zen_mode_from_contacts); } else { return mContext.getString(R.string.zen_mode_from_starred); } + } else if (category == Policy.PRIORITY_CATEGORY_CONVERSATIONS + && policy.priorityConversationSenders + == Policy.CONVERSATION_SENDERS_IMPORTANT) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_important_conversations); + } else { + return mContext.getString( + R.string.zen_mode_from_important_conversations_second); + } } else if (category == Policy.PRIORITY_CATEGORY_EVENTS) { if (isFirst) { return mContext.getString(R.string.zen_mode_events_list_first); diff --git a/src/com/android/settings/notification/zen/ZenPrioritySendersHelper.java b/src/com/android/settings/notification/zen/ZenPrioritySendersHelper.java new file mode 100644 index 00000000000..5d0b71b7643 --- /dev/null +++ b/src/com/android/settings/notification/zen/ZenPrioritySendersHelper.java @@ -0,0 +1,388 @@ +/* + * Copyright (C) 2021 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.zen; + +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; + +import android.app.NotificationManager; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.icu.text.MessageFormat; +import android.provider.Contacts; +import android.service.notification.ConversationChannelWrapper; +import android.view.View; + +import androidx.preference.PreferenceCategory; + +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; +import com.android.settings.notification.NotificationBackend; +import com.android.settings.notification.app.ConversationListSettings; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Shared class implementing priority senders logic to be used both for zen mode and zen custom + * rules, governing which senders can break through DND. This helper class controls creating + * and displaying the relevant preferences for either messages or calls mode, and determining + * what the priority and conversation senders settings should be given a click. + * + * The outer classes govern how those settings are stored -- for instance, where and how they + * are saved, and where they're read from to get current status. + */ +public class ZenPrioritySendersHelper { + public static final String TAG = "ZenPrioritySendersHelper"; + + static final int UNKNOWN = -10; + static final String KEY_ANY = "senders_anyone"; + static final String KEY_CONTACTS = "senders_contacts"; + static final String KEY_STARRED = "senders_starred_contacts"; + static final String KEY_IMPORTANT = "conversations_important"; + static final String KEY_NONE = "senders_none"; + + private int mNumImportantConversations = UNKNOWN; + + private static final Intent ALL_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_DEFAULT) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent STARRED_CONTACTS_INTENT = + new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + private final Context mContext; + private final ZenModeBackend mZenModeBackend; + private final NotificationBackend mNotificationBackend; + private final PackageManager mPackageManager; + private final boolean mIsMessages; // if this is false, then this preference is for calls + private final SelectorWithWidgetPreference.OnClickListener mSelectorClickListener; + + private PreferenceCategory mPreferenceCategory; + private List mSelectorPreferences = new ArrayList<>(); + + public ZenPrioritySendersHelper(Context context, boolean isMessages, + ZenModeBackend zenModeBackend, NotificationBackend notificationBackend, + SelectorWithWidgetPreference.OnClickListener clickListener) { + mContext = context; + mIsMessages = isMessages; + mZenModeBackend = zenModeBackend; + mNotificationBackend = notificationBackend; + mSelectorClickListener = clickListener; + + mPackageManager = mContext.getPackageManager(); + if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { + FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); + } + } + + void displayPreference(PreferenceCategory preferenceCategory) { + mPreferenceCategory = preferenceCategory; + if (mPreferenceCategory.getPreferenceCount() == 0) { + makeSelectorPreference(KEY_STARRED, + com.android.settings.R.string.zen_mode_from_starred, mIsMessages); + makeSelectorPreference(KEY_CONTACTS, + com.android.settings.R.string.zen_mode_from_contacts, mIsMessages); + if (mIsMessages) { + makeSelectorPreference(KEY_IMPORTANT, + com.android.settings.R.string.zen_mode_from_important_conversations, true); + updateChannelCounts(); + } + makeSelectorPreference(KEY_ANY, + com.android.settings.R.string.zen_mode_from_anyone, mIsMessages); + makeSelectorPreference(KEY_NONE, + com.android.settings.R.string.zen_mode_none_messages, mIsMessages); + updateSummaries(); + } + } + + void updateState(int currContactsSetting, int currConversationsSetting) { + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + // for each preference, check whether the current state matches what this state + // would look like if the button were checked. + final int[] checkedState = keyToSettingEndState(pref.getKey(), true); + final int checkedContactsSetting = checkedState[0]; + final int checkedConversationsSetting = checkedState[1]; + + boolean match = checkedContactsSetting == currContactsSetting; + if (mIsMessages && checkedConversationsSetting != UNKNOWN) { + // "UNKNOWN" in checkedContactsSetting means this preference doesn't govern + // the priority senders setting, so the full match happens when either + // the priority senders setting matches or if it's UNKNOWN so only the conversation + // setting needs to match. + match = (match || checkedContactsSetting == UNKNOWN) + && (checkedConversationsSetting == currConversationsSetting); + } + + pref.setChecked(match); + } + } + + void updateSummaries() { + for (SelectorWithWidgetPreference pref : mSelectorPreferences) { + pref.setSummary(getSummary(pref.getKey())); + } + } + + // Gets the desired end state of the priority senders and conversations for the given key + // and whether it is being checked or unchecked. UNKNOWN indicates no change in state. + // + // Returns an integer array with 2 entries. The first entry is the setting for priority senders + // and the second entry is for priority conversation senders; if isMessages is false, then + // no changes will ever be prescribed for conversation senders. + int[] keyToSettingEndState(String key, boolean checked) { + int[] endState = new int[]{ UNKNOWN, UNKNOWN }; + if (!checked) { + // Unchecking any priority-senders-based state should reset the state to NONE. + // "Unchecking" the NONE state doesn't do anything, in practice. + switch (key) { + case KEY_STARRED: + case KEY_CONTACTS: + case KEY_ANY: + case KEY_NONE: + endState[0] = ZenModeBackend.SOURCE_NONE; + } + + // For messages, unchecking "priority conversations" and "any" should reset conversation + // state to "NONE" as well. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + case KEY_ANY: + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } else { + // All below is for the enabling (checked) state. + switch (key) { + case KEY_STARRED: + endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_STARRED; + break; + case KEY_CONTACTS: + endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; + break; + case KEY_ANY: + endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_ANY; + break; + case KEY_NONE: + endState[0] = ZenModeBackend.SOURCE_NONE; + } + + // In the messages case *only*, also handle changing of conversation settings. + if (mIsMessages) { + switch (key) { + case KEY_IMPORTANT: + endState[1] = CONVERSATION_SENDERS_IMPORTANT; + break; + case KEY_ANY: + endState[1] = CONVERSATION_SENDERS_ANYONE; + break; + case KEY_NONE: + endState[1] = CONVERSATION_SENDERS_NONE; + } + } + } + + // Error case check: if somehow, after all of that, endState is still {UNKNOWN, UNKNOWN}, + // something has gone wrong. + if (endState[0] == UNKNOWN && endState[1] == UNKNOWN) { + throw new IllegalArgumentException("invalid key " + key); + } + + return endState; + } + + // Returns the preferences, if any, that should be newly saved for the specified setting and + // checked state in an array where index 0 is the new senders setting and 1 the new + // conversations setting. A return value of UNKNOWN indicates that nothing should change. + // + // The returned conversations setting will always be UNKNOWN (not to change) in the calls case. + // + // Checking and unchecking is mostly an operation of setting or unsetting the relevant + // preference, except for some special handling where the conversation setting overlaps: + // - setting or unsetting "priority contacts" or "contacts" has no effect on the + // priority conversation setting, and vice versa + // - if "priority conversations" is selected, and the user checks "anyone", the conversation + // setting is also set to any conversations + // - if "anyone" is previously selected, and the user clicks "priority conversations", then + // the contacts setting is additionally reset to "none". + // - if "anyone" is previously selected, and the user clicks one of the contacts values, + // then the conversations setting is additionally reset to "none". + int[] settingsToSaveOnClick(SelectorWithWidgetPreference preference, + int currSendersSetting, int currConvosSetting) { + int[] savedSettings = new int[]{ UNKNOWN, UNKNOWN }; + + // If the preference isn't a checkbox, always consider this to be "checking" the setting. + // Otherwise, toggle. + final int[] endState = keyToSettingEndState(preference.getKey(), + preference.isCheckBox() ? !preference.isChecked() : true); + final int prioritySendersSetting = endState[0]; + final int priorityConvosSetting = endState[1]; + + if (prioritySendersSetting != UNKNOWN && prioritySendersSetting != currSendersSetting) { + savedSettings[0] = prioritySendersSetting; + } + + // Only handle conversation settings for the messages case. If not messages, there should + // never be any change to the conversation senders setting. + if (mIsMessages) { + if (priorityConvosSetting != UNKNOWN + && priorityConvosSetting != currConvosSetting) { + savedSettings[1] = priorityConvosSetting; + } + + // Special-case handling for the "priority conversations" checkbox: + // If a specific selection exists for priority senders (starred, contacts), we leave + // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. + if (preference.getKey() == KEY_IMPORTANT + && currSendersSetting == PRIORITY_SENDERS_ANY) { + savedSettings[0] = ZenModeBackend.SOURCE_NONE; + } + + // Flip-side special case for clicking either "contacts" option: if a specific selection + // exists for priority conversations, leave it untouched; otherwise, set to none. + if ((preference.getKey() == KEY_STARRED || preference.getKey() == KEY_CONTACTS) + && currConvosSetting == CONVERSATION_SENDERS_ANYONE) { + savedSettings[1] = CONVERSATION_SENDERS_NONE; + } + } + + return savedSettings; + } + + private String getSummary(String key) { + switch (key) { + case KEY_STARRED: + return mZenModeBackend.getStarredContactsSummary(mContext); + case KEY_CONTACTS: + return mZenModeBackend.getContactsNumberSummary(mContext); + case KEY_IMPORTANT: + return getConversationSummary(); + case KEY_ANY: + return mContext.getResources().getString(mIsMessages + ? R.string.zen_mode_all_messages_summary + : R.string.zen_mode_all_calls_summary); + case KEY_NONE: + default: + return null; + } + } + + private String getConversationSummary() { + final int numConversations = mNumImportantConversations; + + if (numConversations == UNKNOWN) { + return null; + } else { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_conversations_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numConversations); + return msgFormat.format(args); + } + } + + void updateChannelCounts() { + // Load conversations + ParceledListSlice impConversations = + mNotificationBackend.getConversations(true); + int numImportantConversations = 0; + if (impConversations != null) { + for (ConversationChannelWrapper conversation : impConversations.getList()) { + if (!conversation.getNotificationChannel().isDemoted()) { + numImportantConversations++; + } + } + } + mNumImportantConversations = numImportantConversations; + } + + private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId, + boolean isCheckbox) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); + pref.setKey(key); + pref.setTitle(titleId); + pref.setOnClickListener(mSelectorClickListener); + + View.OnClickListener widgetClickListener = getWidgetClickListener(key); + if (widgetClickListener != null) { + pref.setExtraWidgetOnClickListener(widgetClickListener); + } + + mPreferenceCategory.addPreference(pref); + mSelectorPreferences.add(pref); + return pref; + } + + private View.OnClickListener getWidgetClickListener(String key) { + if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) { + return null; + } + + if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { + return null; + } + + if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { + return null; + } + + return new View.OnClickListener() { + @Override + public void onClick(View v) { + if (KEY_STARRED.equals(key) + && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(STARRED_CONTACTS_INTENT); + } else if (KEY_CONTACTS.equals(key) + && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { + mContext.startActivity(ALL_CONTACTS_INTENT); + } else if (KEY_IMPORTANT.equals(key)) { + new SubSettingLauncher(mContext) + .setDestination(ConversationListSettings.class.getName()) + .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) + .launch(); + } else { + mContext.startActivity(FALLBACK_INTENT); + } + } + }; + } + + private boolean isStarredIntentValid() { + return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } + + private boolean isContactsIntentValid() { + return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null + || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; + } +} diff --git a/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceController.java b/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceController.java new file mode 100644 index 00000000000..cee496ee8b0 --- /dev/null +++ b/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceController.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2021 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.zen; + +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.UNKNOWN; + +import android.app.AutomaticZenRule; +import android.content.Context; +import android.os.AsyncTask; +import android.service.notification.ZenPolicy; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +/** + * Shared controller for custom rule priority senders settings for both messages and calls. + * + * Most functionality is the same as that of the main zen mode messages and calls settings; + * these controllers handle which senders are allowed to break through DND for messages or calls, + * with possible settings options being: starred contacts, all contacts, priority conversations + * (for messages only), anyone, or no one. + */ +public class ZenRulePrioritySendersPreferenceController + extends AbstractZenCustomRulePreferenceController { + private final boolean mIsMessages; // if this is false, then this preference is for calls + + private PreferenceCategory mPreferenceCategory; + private ZenPrioritySendersHelper mHelper; + + public ZenRulePrioritySendersPreferenceController(Context context, String key, + Lifecycle lifecycle, boolean isMessages, NotificationBackend notificationBackend) { + super(context, key, lifecycle); + mIsMessages = isMessages; + + mHelper = new ZenPrioritySendersHelper( + context, isMessages, mBackend, notificationBackend, mSelectorClickListener); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + mHelper.displayPreference(mPreferenceCategory); + super.displayPreference(screen); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (mRule != null && mRule.getZenPolicy() != null) { + final int currContactsSetting = getPrioritySenders(); + final int currConversationsSetting = getPriorityConversationSenders(); + mHelper.updateState(currContactsSetting, currConversationsSetting); + } + } + + @Override + public void onResume(AutomaticZenRule rule, String id) { + super.onResume(rule, id); + if (mIsMessages) { + updateChannelCounts(); + } + mHelper.updateSummaries(); + } + + private void updateChannelCounts() { + // Load conversations + new AsyncTask() { + @Override + protected Void doInBackground(Void... unused) { + mHelper.updateChannelCounts(); + return null; + } + + @Override + protected void onPostExecute(Void unused) { + if (mContext == null) { + return; + } + updateState(mPreferenceCategory); + } + }.execute(); + } + + private int getPrioritySenders() { + if (mRule == null || mRule.getZenPolicy() == null) { + return UNKNOWN; + } + if (mIsMessages) { + return ZenModeBackend.getContactSettingFromZenPolicySetting( + mRule.getZenPolicy().getPriorityMessageSenders()); + } else { + return ZenModeBackend.getContactSettingFromZenPolicySetting( + mRule.getZenPolicy().getPriorityCallSenders()); + } + } + + private int getPriorityConversationSenders() { + if (mRule == null || mRule.getZenPolicy() == null) { + return UNKNOWN; + } + return mRule.getZenPolicy().getPriorityConversationSenders(); + } + + // Returns the ZenPolicySetting enum associated with the provided NotificationManager.Policy. + static @ZenPolicy.PeopleType int zenPolicySettingFromSender(int senderSetting) { + return ZenModeBackend.getZenPolicySettingFromPrefKey( + ZenModeBackend.getKeyFromSetting(senderSetting)); + } + + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = + new SelectorWithWidgetPreference.OnClickListener() { + @Override + public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { + if (mRule == null || mRule.getZenPolicy() == null) { + return; + } + + final int[] settingsToSave = mHelper.settingsToSaveOnClick(preference, + getPrioritySenders(), getPriorityConversationSenders()); + final int prioritySendersSetting = settingsToSave[0]; + final int priorityConvosSetting = settingsToSave[1]; + + // if both are UNKNOWN then just return + if (prioritySendersSetting == UNKNOWN && priorityConvosSetting == UNKNOWN) { + return; + } + + if (prioritySendersSetting != UNKNOWN) { + if (mIsMessages) { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowMessages( + zenPolicySettingFromSender(prioritySendersSetting)) + .build()); + } else { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowCalls( + zenPolicySettingFromSender(prioritySendersSetting)) + .build()); + } + } + + if (mIsMessages && priorityConvosSetting != UNKNOWN) { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowConversations(priorityConvosSetting) + .build()); + } + + // Save any changes + mBackend.updateZenRule(mId, mRule); + } + }; +} diff --git a/tests/robotests/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceControllerTest.java index 23dc71a44c9..99fa8e6cdd4 100644 --- a/tests/robotests/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/notification/zen/ZenModePrioritySendersPreferenceControllerTest.java @@ -16,31 +16,30 @@ package com.android.settings.notification.zen; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE; +import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS; import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; -import static com.android.settings.notification.zen.ZenModePrioritySendersPreferenceController.KEY_ANY; -import static com.android.settings.notification.zen.ZenModePrioritySendersPreferenceController.KEY_CONTACTS; -import static com.android.settings.notification.zen.ZenModePrioritySendersPreferenceController.KEY_NONE; -import static com.android.settings.notification.zen.ZenModePrioritySendersPreferenceController.KEY_STARRED; - -import static com.google.common.truth.Truth.assertThat; +import static com.android.settings.notification.zen.ZenModeBackend.SOURCE_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_ANY; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_CONTACTS; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.UNKNOWN; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.NotificationManager; -import android.content.ContentResolver; import android.content.Context; -import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; @@ -51,143 +50,145 @@ import com.android.settingslib.widget.SelectorWithWidgetPreference; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; import org.robolectric.util.ReflectionHelpers; -import java.util.List; - @RunWith(RobolectricTestRunner.class) public class ZenModePrioritySendersPreferenceControllerTest { - private ZenModePrioritySendersPreferenceController mMessagesController; + private ZenModePrioritySendersPreferenceController mCallsController; @Mock private ZenModeBackend mZenBackend; @Mock - private PreferenceCategory mMockPrefCategory; - @Mock - private NotificationManager.Policy mPolicy; + private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory; @Mock private PreferenceScreen mPreferenceScreen; @Mock private NotificationBackend mNotifBackend; + @Mock + private ZenPrioritySendersHelper mHelper; - private List mSelectorWithWidgetPreferences; - private ContentResolver mContentResolver; private Context mContext; + @Before public void setup() { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; mMessagesController = new ZenModePrioritySendersPreferenceController( - mContext, "test_key_messages", mock(Lifecycle.class), true); + mContext, "test_key_messages", mock(Lifecycle.class), true, + mNotifBackend); ReflectionHelpers.setField(mMessagesController, "mBackend", mZenBackend); + ReflectionHelpers.setField(mMessagesController, "mHelper", mHelper); - when(mMockPrefCategory.getContext()).thenReturn(mContext); + mCallsController = new ZenModePrioritySendersPreferenceController( + mContext, "test_key_calls", mock(Lifecycle.class), false, + mNotifBackend); + ReflectionHelpers.setField(mCallsController, "mBackend", mZenBackend); + ReflectionHelpers.setField(mCallsController, "mHelper", mHelper); + + when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext); + when(mMockCallsPrefCategory.getContext()).thenReturn(mContext); when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey())) - .thenReturn(mMockPrefCategory); - captureRadioButtons(); + .thenReturn(mMockMessagesPrefCategory); + when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey())) + .thenReturn(mMockCallsPrefCategory); } @Test - public void displayPreference_radioButtonsCreatedOnlyOnce() { - when(mMockPrefCategory.findPreference(any())).thenReturn(mock(Preference.class)); - - // radio buttons were already created, so don't re-create them + public void displayPreference_delegatesToHelper() { mMessagesController.displayPreference(mPreferenceScreen); - verify(mMockPrefCategory, never()).addPreference(any()); + verify(mHelper, times(1)).displayPreference(mMockMessagesPrefCategory); + + mCallsController.displayPreference(mPreferenceScreen); + verify(mHelper, times(1)).displayPreference(mMockCallsPrefCategory); } @Test - public void clickAnySenders() { - // GIVEN current priority message senders are STARRED + public void clickPreference_Messages() { + // While most of the actual logical functionality for the preference key -> result + // is/should be controlled by the ZenPrioritySendersHelper, here we need to make sure + // the returned values from the helper are successfully passed through the click listener. + + // GIVEN current priority message senders are STARRED and conversation senders NONE when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_STARRED); + when(mZenBackend.getPriorityConversationSenders()).thenReturn(CONVERSATION_SENDERS_NONE); + + // When we ask mHelper for settings to save on click, it returns ANY for senders and + // conversations (what it would return if the user clicked "Anyone") + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE}); // WHEN user clicks the any senders option - SelectorWithWidgetPreference allSendersRb = getButton(KEY_ANY); - allSendersRb.onClick(); + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + anyPref.onClick(); // THEN any senders gets saved as priority senders for messages + // and also allow any conversations verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_MESSAGES, PRIORITY_SENDERS_ANY); + verify(mZenBackend).saveConversationSenders(CONVERSATION_SENDERS_ANYONE); } @Test - public void clickStarredSenders() { - // GIVEN current priority message senders are ANY - when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_ANY); + public void clickPreference_MessagesUnset() { + // Confirm that when asked to not set something, no ZenModeBackend call occurs. + // GIVEN current priority message senders are STARRED and conversation senders NONE + when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_STARRED); + when(mZenBackend.getPriorityConversationSenders()).thenReturn(CONVERSATION_SENDERS_NONE); + + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{SOURCE_NONE, UNKNOWN}); // WHEN user clicks the starred contacts option - SelectorWithWidgetPreference starredRb = getButton(KEY_STARRED); - starredRb.onClick(); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + nonePref.onClick(); - // THEN starred contacts gets saved as priority senders for messages - verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_MESSAGES, PRIORITY_SENDERS_STARRED); + // THEN "none" gets saved as priority senders for messages + verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_MESSAGES, SOURCE_NONE); + + // AND that no changes are made to conversation senders + verify(mZenBackend, never()).saveConversationSenders(anyInt()); } @Test - public void clickContactsSenders() { - // GIVEN current priority message senders are ANY - when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_ANY); + public void clickPreference_Calls() { + // GIVEN current priority call senders are ANY + when(mZenBackend.getPriorityCallSenders()).thenReturn(PRIORITY_SENDERS_ANY); - // WHEN user clicks the contacts only option - SelectorWithWidgetPreference contactsRb = getButton(KEY_CONTACTS); - contactsRb.onClick(); + // (and this shouldn't happen, but also be prepared to give an answer if asked for + // conversation senders) + when(mZenBackend.getPriorityConversationSenders()).thenReturn(CONVERSATION_SENDERS_ANYONE); - // THEN contacts gets saved as priority senders for messages - verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_MESSAGES, PRIORITY_SENDERS_CONTACTS); + // Helper returns what would've happened to set priority senders to contacts + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_NONE}); + + // WHEN user clicks the any senders option + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false); + contactsPref.onClick(); + + // THEN contacts gets saved as priority senders for calls + // and no conversation policies are modified + verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_CONTACTS); + verify(mZenBackend, never()).saveConversationSenders(anyInt()); } - @Test - public void clickNoSenders() { - // GIVEN current priority message senders are ANY - when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_ANY); - - // WHEN user clicks the no senders option - SelectorWithWidgetPreference noSenders = getButton(KEY_NONE); - noSenders.onClick(); - - // THEN no senders gets saved as priority senders for messages - verify(mZenBackend).saveSenders(PRIORITY_CATEGORY_MESSAGES, ZenModeBackend.SOURCE_NONE); - } - - @Test - public void clickSameOptionMultipleTimes() { - // GIVEN current priority message senders are ANY - when(mZenBackend.getPriorityMessageSenders()).thenReturn(PRIORITY_SENDERS_ANY); - - // WHEN user clicks the any senders option multiple times again - SelectorWithWidgetPreference anySenders = getButton(KEY_ANY); - anySenders.onClick(); - anySenders.onClick(); - anySenders.onClick(); - - // THEN no senders are saved because this setting is already in effect - verify(mZenBackend, never()).saveSenders(PRIORITY_CATEGORY_MESSAGES, PRIORITY_SENDERS_ANY); - } - - private void captureRadioButtons() { - ArgumentCaptor rbCaptor = - ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); - mMessagesController.displayPreference(mPreferenceScreen); - - // verifies 4 buttons were added - verify(mMockPrefCategory, times(4)).addPreference(rbCaptor.capture()); - mSelectorWithWidgetPreferences = rbCaptor.getAllValues(); - assertThat(mSelectorWithWidgetPreferences.size()).isEqualTo(4); - - reset(mMockPrefCategory); - } - - private SelectorWithWidgetPreference getButton(String key) { - for (SelectorWithWidgetPreference pref : mSelectorWithWidgetPreferences) { - if (key.equals(pref.getKey())) { - return pref; - } - } - return null; + // Makes a preference with the provided key and whether it's a checkbox with + // mSelectorClickListener as the onClickListener set. + private SelectorWithWidgetPreference makePreference( + String key, boolean isCheckbox, boolean isMessages) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mContext, isCheckbox); + pref.setKey(key); + pref.setOnClickListener( + isMessages ? mMessagesController.mSelectorClickListener + : mCallsController.mSelectorClickListener); + return pref; } } diff --git a/tests/robotests/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceControllerTest.java new file mode 100644 index 00000000000..86abf36e8a9 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/zen/ZenRulePrioritySendersPreferenceControllerTest.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2021 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.zen; + +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; + +import static com.android.settings.notification.zen.ZenModeBackend.SOURCE_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_ANY; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_CONTACTS; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.UNKNOWN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AutomaticZenRule; +import android.app.NotificationManager; +import android.content.Context; +import android.service.notification.ZenPolicy; + +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +public class ZenRulePrioritySendersPreferenceControllerTest { + private ZenRulePrioritySendersPreferenceController mMessagesController; + private ZenRulePrioritySendersPreferenceController mCallsController; + + @Mock + private ZenModeBackend mZenBackend; + @Mock + private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory; + @Mock + private PreferenceScreen mPreferenceScreen; + @Mock + private NotificationBackend mNotifBackend; + @Mock + private ZenPrioritySendersHelper mHelper; + + private Context mContext; + private final String mId = "test_zen_rule_id"; + private AutomaticZenRule mRule; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mRule = new AutomaticZenRule("test", null, null, null, null, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); + mMessagesController = new ZenRulePrioritySendersPreferenceController( + mContext, "test_key_messages", mock(Lifecycle.class), true, + mNotifBackend); + ReflectionHelpers.setField(mMessagesController, "mBackend", mZenBackend); + ReflectionHelpers.setField(mMessagesController, "mHelper", mHelper); + ReflectionHelpers.setField(mMessagesController, "mRule", mRule); + ReflectionHelpers.setField(mMessagesController, "mId", mId); + + mCallsController = new ZenRulePrioritySendersPreferenceController( + mContext, "test_key_calls", mock(Lifecycle.class), false, + mNotifBackend); + ReflectionHelpers.setField(mCallsController, "mBackend", mZenBackend); + ReflectionHelpers.setField(mCallsController, "mHelper", mHelper); + ReflectionHelpers.setField(mCallsController, "mRule", mRule); + ReflectionHelpers.setField(mCallsController, "mId", mId); + + when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext); + when(mMockCallsPrefCategory.getContext()).thenReturn(mContext); + when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey())) + .thenReturn(mMockMessagesPrefCategory); + when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey())) + .thenReturn(mMockCallsPrefCategory); + when(mZenBackend.getAutomaticZenRule(mId)).thenReturn(mRule); + } + + @Test + public void displayPreference_delegatesToHelper() { + mMessagesController.displayPreference(mPreferenceScreen); + verify(mHelper, times(1)).displayPreference(mMockMessagesPrefCategory); + + mCallsController.displayPreference(mPreferenceScreen); + verify(mHelper, times(1)).displayPreference(mMockCallsPrefCategory); + } + + @Test + public void clickPreference_Messages() { + // While most of the actual logical functionality for the preference key -> result + // is/should be controlled by the ZenPrioritySendersHelper, here we need to make sure + // the returned values from the helper are correctly saved to the zen policy in mRule. + + // GIVEN current priority message senders are STARRED and conversation senders NONE + setMessageSenders(PRIORITY_SENDERS_STARRED); + setConversationSenders(CONVERSATION_SENDERS_NONE); + + // When we ask mHelper for settings to save on click, it returns ANY for senders and + // conversations (what it would return if the user clicked "Anyone") + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE}); + + // WHEN user clicks the any senders option + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + anyPref.onClick(); + + // THEN any senders gets saved as priority senders for messages + // and also allow any conversations + assertThat(getMessageSenders()).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(getConversationSenders()).isEqualTo(CONVERSATION_SENDERS_ANYONE); + } + + @Test + public void clickPreference_MessagesUnset() { + // Confirm that when asked to not set something, no change occurs. + // GIVEN current priority message senders are STARRED and conversation senders NONE + setMessageSenders(PRIORITY_SENDERS_STARRED); + setConversationSenders(CONVERSATION_SENDERS_NONE); + + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{SOURCE_NONE, UNKNOWN}); + + // WHEN user clicks the starred contacts option + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + nonePref.onClick(); + + // THEN priority senders for messages is set to NONE + assertThat(getMessageSenders()).isEqualTo(SOURCE_NONE); + + // AND that conversation senders remains unchanged + assertThat(getConversationSenders()).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void clickPreference_Calls() { + // GIVEN current priority call senders are ANY + setCallSenders(PRIORITY_SENDERS_ANY); + + // Helper returns what would've happened to set priority senders to contacts + when(mHelper.settingsToSaveOnClick( + any(SelectorWithWidgetPreference.class), anyInt(), anyInt())) + .thenReturn(new int[]{PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_NONE}); + + // WHEN user clicks the any senders option + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false); + contactsPref.onClick(); + + // THEN contacts gets saved as priority senders for calls + assertThat(getCallSenders()).isEqualTo(PRIORITY_SENDERS_CONTACTS); + } + + private SelectorWithWidgetPreference makePreference( + String key, boolean isCheckbox, boolean isMessages) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mContext, isCheckbox); + pref.setKey(key); + pref.setOnClickListener( + isMessages ? mMessagesController.mSelectorClickListener + : mCallsController.mSelectorClickListener); + return pref; + } + + // Helper methods for setting up and reading current state on mRule. These are mostly helpful + // just to handle translating between the enums used in ZenPolicy from the ones used in + // the settings for message/call senders. + private void setMessageSenders(int messageSenders) { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowMessages( + ZenRulePrioritySendersPreferenceController.zenPolicySettingFromSender( + messageSenders)) + .build()); + } + + private int getMessageSenders() { + return ZenModeBackend.getContactSettingFromZenPolicySetting( + mRule.getZenPolicy().getPriorityMessageSenders()); + } + + private void setCallSenders(int callSenders) { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowCalls( + ZenRulePrioritySendersPreferenceController.zenPolicySettingFromSender( + callSenders)) + .build()); + } + + private int getCallSenders() { + return ZenModeBackend.getContactSettingFromZenPolicySetting( + mRule.getZenPolicy().getPriorityCallSenders()); + } + + // There's no enum conversion on the conversation senders, as they use the same enum, but + // these methods provide some convenient parallel usage compared to the others. + private void setConversationSenders(int conversationSenders) { + mRule.setZenPolicy(new ZenPolicy.Builder(mRule.getZenPolicy()) + .allowConversations(conversationSenders) + .build()); + } + + private int getConversationSenders() { + return mRule.getZenPolicy().getPriorityConversationSenders(); + } +} diff --git a/tests/unit/src/com/android/settings/notification/zen/ZenPrioritySendersHelperTest.java b/tests/unit/src/com/android/settings/notification/zen/ZenPrioritySendersHelperTest.java new file mode 100644 index 00000000000..d56818d04d7 --- /dev/null +++ b/tests/unit/src/com/android/settings/notification/zen/ZenPrioritySendersHelperTest.java @@ -0,0 +1,559 @@ +/* + * Copyright (C) 2021 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.zen; + +import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; +import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_STARRED; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; + +import static com.android.settings.notification.zen.ZenModeBackend.SOURCE_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_ANY; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_CONTACTS; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_IMPORTANT; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_NONE; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.KEY_STARRED; +import static com.android.settings.notification.zen.ZenPrioritySendersHelper.UNKNOWN; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +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.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; + +import androidx.preference.PreferenceCategory; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.notification.NotificationBackend; +import com.android.settingslib.widget.SelectorWithWidgetPreference; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class ZenPrioritySendersHelperTest { + public static final String TAG = "ZenPrioritySendersHelperTest"; + @Mock + private PreferenceCategory mMockPrefCategory; + @Mock + private ZenModeBackend mZenBackend; + @Mock + private NotificationBackend mNotifBackend; + @Mock + private SelectorWithWidgetPreference.OnClickListener mClickListener; + + private Context mContext; + @Mock + private Resources mResources; + @Mock + private ContentResolver mContentResolver; + + // This class is simply a wrapper to override getSummary() in order to avoid ZenModeBackend + // calls. + private class ZenPrioritySendersHelperWrapper extends ZenPrioritySendersHelper { + ZenPrioritySendersHelperWrapper(Context context, boolean isMessages, + ZenModeBackend zenModeBackend, + NotificationBackend notificationBackend, + SelectorWithWidgetPreference.OnClickListener clickListener) { + super(context, isMessages, zenModeBackend, notificationBackend, clickListener); + } + + @Override + void updateSummaries() { + // Do nothing, so we don't try to get summaries from resources. + } + } + + // Extension of ArgumentMatcher to check that a preference argument has the correct preference + // key, but doesn't check any other properties. + private class PrefKeyMatcher implements ArgumentMatcher { + private String mKey; + PrefKeyMatcher(String key) { + mKey = key; + } + + public boolean matches(SelectorWithWidgetPreference pref) { + return pref.getKey() != null && pref.getKey().equals(mKey); + } + + public String toString() { + return "SelectorWithWidgetPreference matcher for key " + mKey; + } + } + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getContentResolver()).thenReturn(mContentResolver); + when(mContext.getResources()).thenReturn(mResources); + when(mMockPrefCategory.getContext()).thenReturn(mContext); + + // We don't care about resource contents, just make sure that attempting to access + // resources doesn't kill the test + when(mResources.getString(anyInt())).thenReturn("testString"); + } + + private ZenPrioritySendersHelper makeMessagesHelper() { + return new ZenPrioritySendersHelperWrapper( + mContext, true, mZenBackend, mNotifBackend, mClickListener); + } + + private ZenPrioritySendersHelper makeCallsHelper() { + return new ZenPrioritySendersHelperWrapper( + mContext, false, mZenBackend, mNotifBackend, mClickListener); + } + + private SelectorWithWidgetPreference makePreference(String key, boolean isCheckbox) { + final SelectorWithWidgetPreference pref = + new SelectorWithWidgetPreference(mContext, isCheckbox); + pref.setKey(key); + return pref; + } + + @Test + public void testDisplayPreferences_makeMessagesPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + messagesHelper.displayPreference(mMockPrefCategory); + + // Starred contacts, Contacts, Priority Conversations, Any, None + verify(mMockPrefCategory, times(5)) + .addPreference(prefCaptor.capture()); + + // First verify that the click listener has not been called yet before we start clicking on + // things. + verify(mClickListener, never()) + .onRadioButtonClicked(any(SelectorWithWidgetPreference.class)); + for (SelectorWithWidgetPreference pref : prefCaptor.getAllValues()) { + // Verify that the click listener got a click on something with this pref key. + pref.onClick(); + verify(mClickListener).onRadioButtonClicked(argThat(new PrefKeyMatcher(pref.getKey()))); + } + } + + @Test + public void testDisplayPreferences_makeCallsPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + ZenPrioritySendersHelper callsHelper = makeCallsHelper(); + callsHelper.displayPreference(mMockPrefCategory); + + // Starred contacts, Contacts, Any, None + verify(mMockPrefCategory, times(4)) + .addPreference(prefCaptor.capture()); + + // Make sure we never have the conversation one + verify(mMockPrefCategory, never()) + .addPreference(argThat(new PrefKeyMatcher(KEY_IMPORTANT))); + + verify(mClickListener, never()) + .onRadioButtonClicked(any(SelectorWithWidgetPreference.class)); + for (SelectorWithWidgetPreference pref : prefCaptor.getAllValues()) { + // Verify that the click listener got a click on something with this pref key. + pref.onClick(); + verify(mClickListener).onRadioButtonClicked(argThat(new PrefKeyMatcher(pref.getKey()))); + } + } + + @Test + public void testDisplayPreferences_createdOnlyOnce() { + // Return a nonzero number of child preference when asked. + // Then when displayPreference is called, it should never make any new preferences. + when(mMockPrefCategory.getPreferenceCount()).thenReturn(4); // already created + ZenPrioritySendersHelper callsHelper = makeCallsHelper(); + callsHelper.displayPreference(mMockPrefCategory); + callsHelper.displayPreference(mMockPrefCategory); + callsHelper.displayPreference(mMockPrefCategory); + + // Even though we called display 3 times we shouldn't add more preferences here. + verify(mMockPrefCategory, never()) + .addPreference(any(SelectorWithWidgetPreference.class)); + } + + @Test + public void testKeyToSettingEndState_messagesCheck() { + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] endState; + + // For KEY_NONE everything should be none. + endState = messagesHelper.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + endState = messagesHelper.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + endState = messagesHelper.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_STARRED); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + endState = messagesHelper.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_CONTACTS); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For priority conversations, we should set the conversations but not priority senders + endState = messagesHelper.keyToSettingEndState(KEY_IMPORTANT, true); + assertThat(endState[0]).isEqualTo(UNKNOWN); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testKeyToSettingEndState_messagesUncheck() { + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] endState; + + // For KEY_NONE, "unchecking" still means "none". + endState = messagesHelper.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY unchecking resets the state to "none". + endState = messagesHelper.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = messagesHelper.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + endState = messagesHelper.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For priority conversations, we should set the conversations but not priority senders + endState = messagesHelper.keyToSettingEndState(KEY_IMPORTANT, false); + assertThat(endState[0]).isEqualTo(UNKNOWN); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testKeyToSettingEndState_callsCheck() { + ZenPrioritySendersHelper callsHelper = makeCallsHelper(); + int[] endState; + + // For all of calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + endState = callsHelper.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For KEY_ANY senders should be ANY. + endState = callsHelper.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For [starred] contacts, we should set the priority senders accordingly + endState = callsHelper.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_STARRED); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + endState = callsHelper.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PRIORITY_SENDERS_CONTACTS); + assertThat(endState[1]).isEqualTo(UNKNOWN); + } + + @Test + public void testKeyToSettingEndState_callsUncheck() { + ZenPrioritySendersHelper callsHelper = makeCallsHelper(); + int[] endState; + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + endState = callsHelper.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For KEY_ANY unchecking resets the state to "none". + endState = callsHelper.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = callsHelper.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + + endState = callsHelper.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(SOURCE_NONE); + assertThat(endState[1]).isEqualTo(UNKNOWN); + } + + @Test + public void testSettingsToSave_messagesNone() { + // Test coming from the same state (don't newly save redundant settings) and coming from + // different states (when settings to save should be "none" for both senders and + // conversations). + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] savedSettings; + + // None preference; not a checkbox (so whenever we click it, it counts as "checking"). + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false); + + // Current settings already none; expect no settings to need to be saved + savedSettings = messagesHelper.settingsToSaveOnClick( + nonePref, SOURCE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Current settings are something else; save the "none" settings + savedSettings = messagesHelper.settingsToSaveOnClick( + nonePref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // One but not the other + savedSettings = messagesHelper.settingsToSaveOnClick( + nonePref, SOURCE_NONE, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + savedSettings = messagesHelper.settingsToSaveOnClick( + nonePref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + } + + @Test + public void testSettingsToSave_messagesAny() { + // Test coming from the same state (don't newly save redundant settings) and coming from + // different states (when settings to save should be "any" for both senders and + // conversations). + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] savedSettings; + + // Any preference; checkbox. + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true); + + // Current settings already none; expect no settings to need to be saved + savedSettings = messagesHelper.settingsToSaveOnClick( + anyPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Current settings are something else; save the "any" settings + savedSettings = messagesHelper.settingsToSaveOnClick( + anyPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // It shouldn't be possible to have a starting state of one but not the other, but + // make sure it works anyway? + savedSettings = messagesHelper.settingsToSaveOnClick( + anyPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + savedSettings = messagesHelper.settingsToSaveOnClick( + anyPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Test that unchecking the box results in a "none" state + anyPref.setChecked(true); + savedSettings = messagesHelper.settingsToSaveOnClick( + anyPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testSettingsToSave_messagesContacts() { + // Test coming from the same state (don't newly save redundant settings) and coming from + // different states. + // In addition, saving either starred or contacts has the special case where if we're + // coming from the "any" state it should also set the conversation senders to none. + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] savedSettings; + + // Test both contacts-related preferences here. + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true); + + // Current settings already the relevant ones; expect no settings to need to be saved + // Note that since these are checkboxes, this state shouldn't be reachable, but check it + // anyway just in case. + savedSettings = messagesHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_STARRED, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = messagesHelper.settingsToSaveOnClick( + contactsPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Current settings are something else (contacts setting or "none"); save new senders + // but do not change conversations. + savedSettings = messagesHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_STARRED); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = messagesHelper.settingsToSaveOnClick( + contactsPref, SOURCE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_CONTACTS); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Special additional case: if the settings are currently "any" for both, we additionally + // reset the conversation settings to none. + savedSettings = messagesHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_STARRED); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + savedSettings = messagesHelper.settingsToSaveOnClick( + contactsPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_CONTACTS); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // Test that un-checking works as well. + starredPref.setChecked(true); + contactsPref.setChecked(true); + + // Make sure we don't overwrite existing conversation senders setting when unchecking + savedSettings = messagesHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = messagesHelper.settingsToSaveOnClick( + contactsPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + } + + @Test + public void testSettingsToSave_messagesConversations() { + // Test coming from the same state (don't newly save redundant settings) and coming from + // different states. + // In addition, saving either starred or contacts has the special case where if we're + // coming from the "any" state it should also set the conversation senders to none. + ZenPrioritySendersHelper messagesHelper = makeMessagesHelper(); + int[] savedSettings; + + SelectorWithWidgetPreference convsPref = makePreference(KEY_IMPORTANT, true); + + // Current settings already the relevant ones; expect no settings to need to be saved + // Note that since these are checkboxes, this state shouldn't be reachable, but check it + // anyway just in case. + savedSettings = messagesHelper.settingsToSaveOnClick( + convsPref, PRIORITY_SENDERS_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Current settings are something else (only actual choice here is "none"); save + // new conversations but do not change senders. + savedSettings = messagesHelper.settingsToSaveOnClick( + convsPref, PRIORITY_SENDERS_CONTACTS, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + + // Special additional case: if the settings are currently "any" for both, we additionally + // reset the senders settings to none. + savedSettings = messagesHelper.settingsToSaveOnClick( + convsPref, PRIORITY_SENDERS_ANY, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + + // Test that un-checking works as well. + convsPref.setChecked(true); + + // Make sure we don't overwrite existing conversation senders setting when unchecking + savedSettings = messagesHelper.settingsToSaveOnClick( + convsPref, PRIORITY_SENDERS_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testSettingsToSave_calls() { + // Simpler test for calls: for each one, test that the relevant ones are saved if not + // already set, and that conversation settings are never changed. + ZenPrioritySendersHelper callsHelper = makeCallsHelper(); + int[] savedSettings; + + // None of the preferences are checkboxes. + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, false); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false); + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, false); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false); + + // Test that if the settings are already what is set, nothing happens. + savedSettings = callsHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_STARRED, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick( + contactsPref, PRIORITY_SENDERS_CONTACTS, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick(anyPref, PRIORITY_SENDERS_ANY, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick(nonePref, SOURCE_NONE, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(UNKNOWN); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + // Test that if the settings are something different, the relevant thing gets saved. + savedSettings = callsHelper.settingsToSaveOnClick( + starredPref, PRIORITY_SENDERS_CONTACTS, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_STARRED); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick( + contactsPref, PRIORITY_SENDERS_ANY, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_CONTACTS); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick(anyPref, SOURCE_NONE, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(PRIORITY_SENDERS_ANY); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + + savedSettings = callsHelper.settingsToSaveOnClick( + nonePref, PRIORITY_SENDERS_STARRED, UNKNOWN); + assertThat(savedSettings[0]).isEqualTo(SOURCE_NONE); + assertThat(savedSettings[1]).isEqualTo(UNKNOWN); + } +}