From 2bd1799fb9924f4b53a5b8767ab3521d2383c5a4 Mon Sep 17 00:00:00 2001 From: Julia Reynolds Date: Fri, 17 May 2024 09:34:31 -0400 Subject: [PATCH] Migrate people settings to new modes UI Flag: android.app.modes_ui Bug: 337079247 Test: make -j RunSettingsRoboTests ROBOTEST_FILTER=com.android.settings.notification.modes Change-Id: If5e7b82a006e856b4aceca7acdfc8cc0d11092a7 --- res/xml/modes_calls_settings.xml | 37 ++ res/xml/modes_messages_settings.xml | 29 + res/xml/modes_people_settings.xml | 35 ++ res/xml/modes_rule_settings.xml | 4 + .../modes/ZenModeCallsFragment.java | 61 +++ .../ZenModeCallsLinkPreferenceController.java | 49 ++ .../notification/modes/ZenModeFragment.java | 5 +- .../modes/ZenModeMessagesFragment.java | 57 ++ ...nModeMessagesLinkPreferenceController.java | 51 ++ .../modes/ZenModePeopleFragment.java | 53 ++ ...ZenModePeopleLinkPreferenceController.java | 54 ++ ...dePrioritySendersPreferenceController.java | 447 +++++++++++++++ ...ModeRepeatCallersPreferenceController.java | 82 +++ .../modes/ZenModeSummaryHelper.java | 295 ++++++++++ .../notification/modes/ZenModesBackend.java | 58 ++ .../modes/ZenModesFragmentBase.java | 8 +- ...ModeCallsLinkPreferenceControllerTest.java | 80 +++ ...eMessagesLinkPreferenceControllerTest.java | 77 +++ ...odePeopleLinkPreferenceControllerTest.java | 77 +++ ...ioritySendersPreferenceControllerTest.java | 509 ++++++++++++++++++ .../modes/ZenModesSummaryHelperTest.java | 88 +++ 21 files changed, 2152 insertions(+), 4 deletions(-) create mode 100644 res/xml/modes_calls_settings.xml create mode 100644 res/xml/modes_messages_settings.xml create mode 100644 res/xml/modes_people_settings.xml create mode 100644 src/com/android/settings/notification/modes/ZenModeCallsFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeMessagesFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModePeopleFragment.java create mode 100644 src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java create mode 100644 src/com/android/settings/notification/modes/ZenModeSummaryHelper.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java create mode 100644 tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java diff --git a/res/xml/modes_calls_settings.xml b/res/xml/modes_calls_settings.xml new file mode 100644 index 00000000000..f2ba7f13867 --- /dev/null +++ b/res/xml/modes_calls_settings.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/res/xml/modes_messages_settings.xml b/res/xml/modes_messages_settings.xml new file mode 100644 index 00000000000..d4aee3d1947 --- /dev/null +++ b/res/xml/modes_messages_settings.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/res/xml/modes_people_settings.xml b/res/xml/modes_people_settings.xml new file mode 100644 index 00000000000..136a357dddc --- /dev/null +++ b/res/xml/modes_people_settings.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/res/xml/modes_rule_settings.xml b/res/xml/modes_rule_settings.xml index 1b79153cf7b..12a683e6bec 100644 --- a/res/xml/modes_rule_settings.xml +++ b/res/xml/modes_rule_settings.xml @@ -22,4 +22,8 @@ android:key="header" android:layout="@layout/settings_entity_header" /> + + \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ZenModeCallsFragment.java b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java new file mode 100644 index 00000000000..4c85bf56727 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsFragment.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * DND Calls Settings page to determine which priority senders can bypass DND when this mode is + * activated. + */ +public class ZenModeCallsFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_calls", false, mBackend)); + controllers.add(new ZenModeRepeatCallersPreferenceController(context, + "zen_mode_repeat_callers", mBackend, + context.getResources().getInteger(com.android.internal.R.integer + .config_zen_repeat_callers_threshold))); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_calls_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_CALLS; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java new file mode 100644 index 00000000000..1d1d7505944 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceController.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeCallsLinkPreferenceController extends AbstractZenModePreferenceController { + + private ZenModeSummaryHelper mSummaryHelper; + + public ZenModeCallsLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeCallsFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getCallsSettingSummary(getMode())); + } +} \ No newline at end of file diff --git a/src/com/android/settings/notification/modes/ZenModeFragment.java b/src/com/android/settings/notification/modes/ZenModeFragment.java index d2126810358..1626d91bd85 100644 --- a/src/com/android/settings/notification/modes/ZenModeFragment.java +++ b/src/com/android/settings/notification/modes/ZenModeFragment.java @@ -20,9 +20,6 @@ import android.app.AutomaticZenRule; import android.app.settings.SettingsEnums; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; - import com.android.settings.R; import com.android.settingslib.core.AbstractPreferenceController; @@ -42,6 +39,8 @@ public class ZenModeFragment extends ZenModeFragmentBase { // {@link AbstractZenModePreferenceController}. List prefControllers = new ArrayList<>(); prefControllers.add(new ZenModeHeaderController(context, "header", this, mBackend)); + prefControllers.add(new ZenModePeopleLinkPreferenceController( + context, "zen_mode_people", mBackend)); return prefControllers; } diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java new file mode 100644 index 00000000000..c86f8dd4750 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesFragment.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * DND Messages Settings page to determine which priority senders can bypass DND. + * "Messages" include SMS, MMS, and messaging apps. + */ +public class ZenModeMessagesFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List controllers = new ArrayList<>(); + controllers.add(new ZenModePrioritySendersPreferenceController(context, + "zen_mode_settings_category_messages", true, mBackend)); + return controllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_messages_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_MESSAGES; + } + + @Override + public void onResume() { + super.onResume(); + use(ZenModePrioritySendersPreferenceController.class).onResume(); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java new file mode 100644 index 00000000000..8261008aaf4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceController.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.core.SubSettingLauncher; + +public class ZenModeMessagesLinkPreferenceController extends AbstractZenModePreferenceController { + private final ZenModeSummaryHelper mSummaryHelper; + + public ZenModeMessagesLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(context, backend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModeMessagesFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + + preference.setEnabled(true); + preference.setSummary(mSummaryHelper.getMessagesSettingSummary(getMode().getPolicy())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleFragment.java b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java new file mode 100644 index 00000000000..e1f753cef05 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleFragment.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import com.android.settings.R; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; + +/** + * Settings page that shows what calls and messages will break through the mode and links to the + * configuration pages for both. + */ +public class ZenModePeopleFragment extends ZenModeFragmentBase { + + @Override + protected List createPreferenceControllers(Context context) { + List prefControllers = new ArrayList<>(); + prefControllers.add(new ZenModeCallsLinkPreferenceController( + context, "zen_mode_people_calls", mBackend)); + prefControllers.add(new ZenModeMessagesLinkPreferenceController( + context, "zen_mode_people_messages", mBackend)); + return prefControllers; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.modes_people_settings; + } + + @Override + public int getMetricsCategory() { + // TODO: b/332937635 - make this the correct metrics category + return SettingsEnums.DND_PEOPLE; + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java new file mode 100644 index 00000000000..f12200627ef --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceController.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + + +import static com.android.settings.notification.modes.ZenModeFragmentBase.MODE_ID; + +import android.content.Context; +import android.os.Bundle; +import androidx.preference.Preference; +import com.android.settings.R; +import com.android.settings.core.SubSettingLauncher; + +/** + * Preference with a link and summary about what calls and messages can break through the mode + */ +public class ZenModePeopleLinkPreferenceController extends AbstractZenModePreferenceController { + + ZenModeSummaryHelper mSummaryHelper; + + public ZenModePeopleLinkPreferenceController(Context context, String key, + ZenModesBackend backend) { + super(context, key, backend); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + Bundle bundle = new Bundle(); + bundle.putString(MODE_ID, getMode().getId()); + // TODO(b/332937635): Update metrics category + preference.setIntent(new SubSettingLauncher(mContext) + .setDestination(ZenModePeopleFragment.class.getName()) + .setSourceMetricsCategory(0) + .setArguments(bundle) + .toIntent()); + preference.setSummary(mSummaryHelper.getPeopleSummary(getMode())); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java new file mode 100644 index 00000000000..a71bbe844b4 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; + +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.service.notification.ZenPolicy; +import android.view.View; +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.core.SubSettingLauncher; +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; + +/** + * Common preference controller functionality for zen mode priority senders preferences for both + * messages and calls. + * + * These controllers handle the settings regarding which priority senders that are allowed to + * bypass DND for calls or messages, which may be one of the following values: starred contacts, all + * contacts, priority conversations (for messages only), anyone, or no one. + */ +public class ZenModePrioritySendersPreferenceController + extends AbstractZenModePreferenceController { + private final boolean mIsMessages; // if this is false, then this preference is for calls + + 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 = CONVERSATION_SENDERS_UNSET; + + 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 PreferenceCategory mPreferenceCategory; + private List mSelectorPreferences = new ArrayList<>(); + + private final ZenModeSummaryHelper mZenModeSummaryHelper; + + public ZenModePrioritySendersPreferenceController(Context context, String key, + boolean isMessages, ZenModesBackend backend) { + super(context, key, backend); + mIsMessages = isMessages; + + String contactsPackage = context.getString(R.string.config_contacts_package_name); + ALL_CONTACTS_INTENT.setPackage(contactsPackage); + STARRED_CONTACTS_INTENT.setPackage(contactsPackage); + FALLBACK_INTENT.setPackage(contactsPackage); + + mPackageManager = mContext.getPackageManager(); + if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { + FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); + } + mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + mPreferenceCategory = screen.findPreference(getPreferenceKey()); + 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); + } + 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); + } + super.displayPreference(screen); + } + + @Override + public void updateState(Preference preference) { + if (mIsMessages) { + updateChannelCounts(); + } + final int currContactsSetting = getPrioritySenders(); + final int currConversationsSetting = getPriorityConversationSenders(); + 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 != CONVERSATION_SENDERS_UNSET) { + // "CONVERSATION_SENDERS_UNSET" 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 CONVERSATION_SENDERS_UNSET + // so only the conversation setting needs to match. + match = (match || checkedContactsSetting == PEOPLE_TYPE_UNSET) + && (checkedConversationsSetting == currConversationsSetting); + } + + pref.setChecked(match); + } + updateSummaries(); + } + + public void onResume() { + if (mIsMessages) { + updateChannelCounts(); + } + updateSummaries(); + } + + private void updateChannelCounts() { + ParceledListSlice impConversations = + mBackend.getConversations(true); + int numImportantConversations = 0; + if (impConversations != null) { + for (ConversationChannelWrapper conversation : impConversations.getList()) { + if (!conversation.getNotificationChannel().isDemoted()) { + numImportantConversations++; + } + } + } + mNumImportantConversations = numImportantConversations; + } + + private int getPrioritySenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityMessageSenders(); + } else { + return getMode().getPolicy().getPriorityCallSenders(); + } + } + + private int getPriorityConversationSenders() { + if (mIsMessages) { + return getMode().getPolicy().getPriorityConversationSenders(); + } + return CONVERSATION_SENDERS_UNSET; + } + + 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 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; + } + + 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. [type]_UNSET 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[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + 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] = PEOPLE_TYPE_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] = PEOPLE_TYPE_STARRED; + break; + case KEY_CONTACTS: + endState[0] = PEOPLE_TYPE_CONTACTS; + break; + case KEY_ANY: + endState[0] = PEOPLE_TYPE_ANYONE; + break; + case KEY_NONE: + endState[0] = PEOPLE_TYPE_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 + // {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong. + if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) { + 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 [type]_UNSET indicates that nothing should + // change. + // + // The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (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[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; + + // 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 != PEOPLE_TYPE_UNSET + && 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 != CONVERSATION_SENDERS_UNSET + && 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 == PEOPLE_TYPE_ANYONE) { + savedSettings[0] = PEOPLE_TYPE_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 mZenModeSummaryHelper.getStarredContactsSummary(); + case KEY_CONTACTS: + return mZenModeSummaryHelper.getContactsNumberSummary(); + 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 == CONVERSATION_SENDERS_UNSET) { + 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); + } + } + + @VisibleForTesting + SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = + new SelectorWithWidgetPreference.OnClickListener() { + @Override + public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { + // The settingsToSaveOnClick function takes whether the preference is a + // checkbox into account to determine whether this selection is checked or unchecked. + final int[] settingsToSave = settingsToSaveOnClick(preference, + getPrioritySenders(), getPriorityConversationSenders()); + final int prioritySendersSetting = settingsToSave[0]; + final int priorityConvosSetting = settingsToSave[1]; + + ZenPolicy.Builder diffPolicy = new ZenPolicy.Builder(); + if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { + if (mIsMessages) { + diffPolicy.allowMessages(prioritySendersSetting); + + } else { + diffPolicy.allowCalls(prioritySendersSetting); + } + } + if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { + diffPolicy.allowConversations(priorityConvosSetting); + } + getMode().setPolicy(diffPolicy.build()); + mBackend.updateMode(getMode()); + } + }; +} diff --git a/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java new file mode 100644 index 00000000000..d6de9c22d3c --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeRepeatCallersPreferenceController.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.STATE_ALLOW; + +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.provider.Settings; +import android.service.notification.ZenPolicy; +import android.util.Log; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; +import com.android.settings.R; + +public class ZenModeRepeatCallersPreferenceController extends AbstractZenModePreferenceController + implements Preference.OnPreferenceChangeListener { + + private final int mRepeatCallersThreshold; + + public ZenModeRepeatCallersPreferenceController(Context context, + String key, ZenModesBackend backend, int repeatCallersThreshold) { + super(context, key, backend); + + mRepeatCallersThreshold = repeatCallersThreshold; + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + + TwoStatePreference pref = (TwoStatePreference) preference; + + boolean anyCallersCanBypassDnd = + getMode().getPolicy().getPriorityCategoryCalls() == STATE_ALLOW + && getMode().getPolicy().getPriorityCallSenders() == PEOPLE_TYPE_ANYONE; + // if any caller can bypass dnd then repeat callers preference is disabled + if (anyCallersCanBypassDnd) { + pref.setEnabled(false); + pref.setChecked(true); + } else { + pref.setEnabled(true); + pref.setChecked( + getMode().getPolicy().getPriorityCategoryRepeatCallers() == STATE_ALLOW); + } + + setRepeatCallerSummary(preference); + } + + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + final boolean allowRepeatCallers = (Boolean) newValue; + ZenPolicy diffPolicy = new ZenPolicy.Builder() + .allowRepeatCallers(allowRepeatCallers) + .build(); + getMode().setPolicy(diffPolicy); + mBackend.updateMode(getMode()); + return true; + } + + private void setRepeatCallerSummary(Preference preference) { + preference.setSummary(mContext.getString(R.string.zen_mode_repeat_callers_summary, + mRepeatCallersThreshold)); + } +} diff --git a/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java new file mode 100644 index 00000000000..cf0c3db7499 --- /dev/null +++ b/src/com/android/settings/notification/modes/ZenModeSummaryHelper.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.settings.notification.modes; + +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CALLS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CONVERSATIONS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_EVENTS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MEDIA; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_MESSAGES; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REMINDERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_REPEAT_CALLERS; +import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_SYSTEM; + +import android.content.Context; +import android.icu.text.MessageFormat; +import android.service.notification.ZenPolicy; + +import com.android.settings.R; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.Predicate; + +public class ZenModeSummaryHelper { + + private Context mContext; + private ZenModesBackend mBackend; + + public ZenModeSummaryHelper(Context context, ZenModesBackend backend) { + mContext = context; + mBackend = backend; + } + + private static final int[] ALL_PRIORITY_CATEGORIES = { + PRIORITY_CATEGORY_ALARMS, + PRIORITY_CATEGORY_MEDIA, + PRIORITY_CATEGORY_SYSTEM, + PRIORITY_CATEGORY_MESSAGES, + PRIORITY_CATEGORY_CONVERSATIONS, + PRIORITY_CATEGORY_EVENTS, + PRIORITY_CATEGORY_REMINDERS, + PRIORITY_CATEGORY_CALLS, + PRIORITY_CATEGORY_REPEAT_CALLERS, + }; + + String getOtherSoundCategoriesSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories( + zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_ALARMS == category + || PRIORITY_CATEGORY_MEDIA == category + || PRIORITY_CATEGORY_SYSTEM == category + || PRIORITY_CATEGORY_REMINDERS == category + || PRIORITY_CATEGORY_EVENTS == category, + true); + int numCategories = enabledCategories.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_other_sounds_summary), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numCategories); + if (numCategories >= 1) { + args.put("sound_category_1", enabledCategories.get(0)); + if (numCategories >= 2) { + args.put("sound_category_2", enabledCategories.get(1)); + if (numCategories == 3) { + args.put("sound_category_3", enabledCategories.get(2)); + } + } + } + return msgFormat.format(args); + } + + String getCallsSettingSummary(ZenMode zenMode) { + List enabledCategories = getEnabledCategories(zenMode.getPolicy(), + category -> PRIORITY_CATEGORY_CALLS == category + || PRIORITY_CATEGORY_REPEAT_CALLERS == category, true); + int numCategories = enabledCategories.size(); + if (numCategories == 0) { + return mContext.getString(R.string.zen_mode_none_calls); + } else if (numCategories == 1) { + return mContext.getString(R.string.zen_mode_calls_summary_one, + enabledCategories.get(0)); + } else { + return mContext.getString(R.string.zen_mode_calls_summary_two, + enabledCategories.get(0), + enabledCategories.get(1)); + } + } + + String getMessagesSettingSummary(ZenPolicy policy) { + List enabledCategories = getEnabledCategories(policy, + 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 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)); + } + } + + String getBlockedEffectsSummary(ZenMode zenMode) { + if (zenMode.getPolicy().shouldShowAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_muted); + } else if (zenMode.getPolicy().shouldHideAllVisualEffects()) { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_hidden); + } else { + return mContext.getResources().getString( + R.string.zen_mode_restrict_notifications_summary_custom); + } + } + + private List getEnabledCategories(ZenPolicy policy, + Predicate filteredCategories, boolean capitalizeFirstInList) { + List enabledCategories = new ArrayList<>(); + for (int category : ALL_PRIORITY_CATEGORIES) { + boolean isFirst = capitalizeFirstInList && enabledCategories.isEmpty(); + if (filteredCategories.test(category) && policy.isCategoryAllowed(category, false)) { + if (category == PRIORITY_CATEGORY_REPEAT_CALLERS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CALLS, false) + && policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + continue; + } + + // For conversations, only the "priority conversations" setting is relevant; any + // other setting is subsumed by the messages-specific messaging. + if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.isCategoryAllowed(PRIORITY_CATEGORY_CONVERSATIONS, false) + && policy.getPriorityConversationSenders() + != CONVERSATION_SENDERS_IMPORTANT) { + continue; + } + + enabledCategories.add(getCategory(category, policy, isFirst)); + } + } + return enabledCategories; + } + + private String getCategory(int category, ZenPolicy policy, boolean isFirst) { + if (category == PRIORITY_CATEGORY_ALARMS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_alarms_list_first); + } else { + return mContext.getString(R.string.zen_mode_alarms_list); + } + } else if (category == PRIORITY_CATEGORY_MEDIA) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_media_list_first); + } else { + return mContext.getString(R.string.zen_mode_media_list); + } + } else if (category == PRIORITY_CATEGORY_SYSTEM) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_system_list_first); + } else { + return mContext.getString(R.string.zen_mode_system_list); + } + } else if (category == PRIORITY_CATEGORY_MESSAGES) { + if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_ANYONE) { + return mContext.getString(R.string.zen_mode_from_anyone); + } else if (policy.getPriorityMessageSenders() == PEOPLE_TYPE_CONTACTS) { + return mContext.getString(R.string.zen_mode_from_contacts); + } else { + return mContext.getString(R.string.zen_mode_from_starred); + } + } else if (category == PRIORITY_CATEGORY_CONVERSATIONS + && policy.getPriorityConversationSenders() == 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 == PRIORITY_CATEGORY_EVENTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_events_list_first); + } else { + return mContext.getString(R.string.zen_mode_events_list); + } + } else if (category == PRIORITY_CATEGORY_REMINDERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_reminders_list_first); + } else { + return mContext.getString(R.string.zen_mode_reminders_list); + } + } else if (category == PRIORITY_CATEGORY_CALLS) { + if (policy.getPriorityCallSenders() == PEOPLE_TYPE_ANYONE) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_anyone); + } + return mContext.getString(R.string.zen_mode_all_callers); + } else if (policy.getPriorityCallSenders() == PEOPLE_TYPE_CONTACTS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_contacts); + } + return mContext.getString(R.string.zen_mode_contacts_callers); + } else { + if (isFirst) { + return mContext.getString(R.string.zen_mode_from_starred); + } + return mContext.getString(R.string.zen_mode_starred_callers); + } + } else if (category == PRIORITY_CATEGORY_REPEAT_CALLERS) { + if (isFirst) { + return mContext.getString(R.string.zen_mode_repeat_callers); + } else { + return mContext.getString(R.string.zen_mode_repeat_callers_list); + } + } + + return ""; + } + + public String getStarredContactsSummary() { + List starredContacts = mBackend.getStarredContacts(); + int numStarredContacts = starredContacts.size(); + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_starred_contacts_summary_contacts), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", numStarredContacts); + if (numStarredContacts >= 1) { + args.put("contact_1", starredContacts.get(0)); + if (numStarredContacts >= 2) { + args.put("contact_2", starredContacts.get(1)); + if (numStarredContacts == 3) { + args.put("contact_3", starredContacts.get(2)); + } + } + } + return msgFormat.format(args); + } + + public String getContactsNumberSummary() { + MessageFormat msgFormat = new MessageFormat( + mContext.getString(R.string.zen_mode_contacts_count), + Locale.getDefault()); + Map args = new HashMap<>(); + args.put("count", mBackend.queryAllContactsData().getCount()); + return msgFormat.format(args); + } + + public String getPeopleSummary(ZenMode zenMode) { + final int callersAllowed = zenMode.getPolicy().getPriorityCallSenders(); + final int messagesAllowed = zenMode.getPolicy().getPriorityMessageSenders(); + final int conversationsAllowed = zenMode.getPolicy().getPriorityConversationSenders(); + final boolean areRepeatCallersAllowed = + zenMode.getPolicy().isCategoryAllowed(PRIORITY_CATEGORY_REPEAT_CALLERS, false); + + if (callersAllowed == PEOPLE_TYPE_ANYONE + && messagesAllowed == PEOPLE_TYPE_ANYONE + && conversationsAllowed == CONVERSATION_SENDERS_ANYONE) { + return mContext.getResources().getString(R.string.zen_mode_people_all); + } else if (callersAllowed == PEOPLE_TYPE_NONE + && messagesAllowed == PEOPLE_TYPE_NONE + && conversationsAllowed == CONVERSATION_SENDERS_NONE + && !areRepeatCallersAllowed) { + return mContext.getResources().getString(R.string.zen_mode_people_none); + } else { + return mContext.getResources().getString(R.string.zen_mode_people_some); + } + } +} diff --git a/src/com/android/settings/notification/modes/ZenModesBackend.java b/src/com/android/settings/notification/modes/ZenModesBackend.java index 355adb46198..ac170c6d3d9 100644 --- a/src/com/android/settings/notification/modes/ZenModesBackend.java +++ b/src/com/android/settings/notification/modes/ZenModesBackend.java @@ -21,14 +21,22 @@ import static java.util.Objects.requireNonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.app.AutomaticZenRule; +import android.app.INotificationManager; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.ParceledListSlice; +import android.database.Cursor; import android.net.Uri; +import android.os.ServiceManager; +import android.provider.ContactsContract; import android.provider.Settings; import android.service.notification.Condition; +import android.service.notification.ConversationChannelWrapper; import android.service.notification.ZenAdapters; import android.service.notification.ZenModeConfig; +import android.util.Log; +import androidx.annotation.VisibleForTesting; import com.android.settings.R; import java.time.Duration; @@ -51,6 +59,8 @@ class ZenModesBackend { private static ZenModesBackend sInstance; private final NotificationManager mNotificationManager; + static INotificationManager sINM = INotificationManager.Stub.asInterface( + ServiceManager.getService(Context.NOTIFICATION_SERVICE)); private final Context mContext; @@ -105,6 +115,54 @@ class ZenModesBackend { } } + public ParceledListSlice getConversations(boolean onlyImportant) { + try { + return sINM.getConversations(onlyImportant); + } catch (Exception e) { + Log.w(TAG, "Error calling NoMan", e); + return ParceledListSlice.emptyList(); + } + } + + public List getStarredContacts() { + Cursor cursor = null; + try { + cursor = queryStarredContactsData(); + return getStarredContacts(cursor); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + @VisibleForTesting + List getStarredContacts(Cursor cursor) { + List starredContacts = new ArrayList<>(); + if (cursor != null && cursor.moveToFirst()) { + do { + String contact = cursor.getString(0); + starredContacts.add(contact != null ? contact : + mContext.getString(R.string.zen_mode_starred_contacts_empty_name)); + + } while (cursor.moveToNext()); + } + return starredContacts; + } + + private Cursor queryStarredContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + ContactsContract.Data.STARRED + "=1", null, + ContactsContract.Data.TIMES_CONTACTED); + } + + Cursor queryAllContactsData() { + return mContext.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, + new String[]{ContactsContract.Contacts.DISPLAY_NAME_PRIMARY}, + null, null, null); + } + private ZenMode getManualDndMode(ZenModeConfig config) { // TODO: b/333530553 - Read ZenDeviceEffects of manual DND. // TODO: b/333682392 - Replace with final strings for name & trigger description diff --git a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java index 3b6d17f4225..595f1d08f29 100644 --- a/src/com/android/settings/notification/modes/ZenModesFragmentBase.java +++ b/src/com/android/settings/notification/modes/ZenModesFragmentBase.java @@ -57,9 +57,9 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { @Override public void onAttach(@NonNull Context context) { - super.onAttach(context); mContext = context; mBackend = ZenModesBackend.getInstance(context); + super.onAttach(context); } @Override @@ -77,6 +77,12 @@ abstract class ZenModesFragmentBase extends RestrictedDashboardFragment { } } + @Override + public void onResume() { + super.onResume(); + updateZenModeState(); + } + @Override public void onStop() { super.onStop(); diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..04d625a2848 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeCallsLinkPreferenceControllerTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowApplication; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeCallsLinkPreferenceControllerTest { + + private ZenModeCallsLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeCallsLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..cfeefb40a43 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModeMessagesLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModeMessagesLinkPreferenceControllerTest { + + private ZenModeMessagesLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModeMessagesLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java new file mode 100644 index 00000000000..81e64648a33 --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePeopleLinkPreferenceControllerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.app.AutomaticZenRule; +import android.app.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.Preference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public final class ZenModePeopleLinkPreferenceControllerTest { + + private ZenModePeopleLinkPreferenceController mController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mController = new ZenModePeopleLinkPreferenceController( + mContext, "something", mBackend); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_UI) + public void testHasSummary() { + Preference pref = mock(Preference.class); + ZenMode zenMode = new ZenMode("id", + new AutomaticZenRule.Builder("Driving", Uri.parse("drive")) + .setType(AutomaticZenRule.TYPE_DRIVING) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowAllSounds().build()) + .build(), true); + mController.updateZenMode(pref, zenMode); + verify(pref).setSummary(any()); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java new file mode 100644 index 00000000000..91eb59a77dc --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceControllerTest.java @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; +import static android.service.notification.ZenPolicy.STATE_ALLOW; +import static android.service.notification.ZenPolicy.STATE_DISALLOW; +import static android.service.notification.ZenPolicy.STATE_UNSET; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_ANY; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_CONTACTS; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_IMPORTANT; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_NONE; +import static com.android.settings.notification.modes.ZenModePrioritySendersPreferenceController.KEY_STARRED; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.never; +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.Flags; +import android.content.Context; +import android.net.Uri; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; +import android.service.notification.ZenPolicy; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceScreen; +import com.android.settingslib.widget.SelectorWithWidgetPreference; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +@EnableFlags(Flags.FLAG_MODES_UI) +public final class ZenModePrioritySendersPreferenceControllerTest { + + private ZenModePrioritySendersPreferenceController mCallsController; + private ZenModePrioritySendersPreferenceController mMessagesController; + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private Context mContext; + @Mock + private ZenModesBackend mBackend; + + @Mock + private PreferenceCategory mMockMessagesPrefCategory, mMockCallsPrefCategory; + @Mock + private PreferenceScreen mPreferenceScreen; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application; + + mMessagesController = new ZenModePrioritySendersPreferenceController( + mContext, "messages", true, mBackend); + mCallsController = new ZenModePrioritySendersPreferenceController( + mContext, "calls", false, mBackend); + when(mMockMessagesPrefCategory.getContext()).thenReturn(mContext); + when(mMockCallsPrefCategory.getContext()).thenReturn(mContext); + when(mPreferenceScreen.findPreference(mMessagesController.getPreferenceKey())) + .thenReturn(mMockMessagesPrefCategory); + when(mPreferenceScreen.findPreference(mCallsController.getPreferenceKey())) + .thenReturn(mMockCallsPrefCategory); + } + + // 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; + } + + // 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; + } + } + + @Test + public void testDisplayPreferences_makeMessagesPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockMessagesPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mMessagesController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Priority Conversations, Any, None + verify(mMockMessagesPrefCategory, times(5)).addPreference(prefCaptor.capture()); + } + + @Test + public void testDisplayPreferences_makeCallsPrefs() { + ArgumentCaptor prefCaptor = + ArgumentCaptor.forClass(SelectorWithWidgetPreference.class); + when(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(0); // not yet created + mCallsController.displayPreference(mPreferenceScreen); + + // Starred contacts, Contacts, Any, None + verify(mMockCallsPrefCategory, times(4)).addPreference(prefCaptor.capture()); + + // Make sure we never have the conversation one + verify(mMockCallsPrefCategory, never()) + .addPreference(argThat(new PrefKeyMatcher(KEY_IMPORTANT))); + } + + @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(mMockCallsPrefCategory.getPreferenceCount()).thenReturn(4); // already created + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + mCallsController.displayPreference(mPreferenceScreen); + + // Even though we called display 3 times we shouldn't add more preferences here. + verify(mMockCallsPrefCategory, never()) + .addPreference(any(SelectorWithWidgetPreference.class)); + } + + @Test + public void testKeyToSettingEndState_messagesCheck() { + int[] endState; + + // For KEY_NONE everything should be none. + endState = mMessagesController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + endState = mMessagesController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testKeyToSettingEndState_messagesUncheck() { + int[] endState; + + // For KEY_NONE, "unchecking" still means "none". + endState = mMessagesController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY unchecking resets the state to "none". + endState = mMessagesController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mMessagesController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mMessagesController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + endState = mMessagesController.keyToSettingEndState(KEY_IMPORTANT, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testKeyToSettingEndState_callsCheck() { + int[] endState; + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + endState = mCallsController.keyToSettingEndState(KEY_NONE, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + endState = mCallsController.keyToSettingEndState(KEY_ANY, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + endState = mCallsController.keyToSettingEndState(KEY_STARRED, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, true); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testKeyToSettingEndState_callsUncheck() { + int[] endState; + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + endState = mCallsController.keyToSettingEndState(KEY_NONE, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + endState = mCallsController.keyToSettingEndState(KEY_ANY, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + endState = mCallsController.keyToSettingEndState(KEY_STARRED, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + endState = mCallsController.keyToSettingEndState(KEY_CONTACTS, false); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_messagesCheck() { + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + int[] endState; + + // For KEY_NONE everything should be none. + nonePref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For KEY_ANY everything should be allowed. + anyPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_ANYONE); + + // For [starred] contacts, we should set the priority senders, but not the conversations + starredPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(true); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_IMPORTANT); + } + + @Test + public void testSettingsToSaveOnClick_messagesUncheck() { + int[] endState; + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For priority conversations, we should set the conversations but not priority senders + impPref.setChecked(false); + endState = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_NONE); + } + + @Test + public void testSettingsToSaveOnClick_callsCheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // For calls: we should never set conversations, as this is unrelated to calls. + // For KEY_NONE senders should be none. + nonePref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY senders should be ANY. + anyPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_ANYONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should set the priority senders accordingly + starredPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_STARRED); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(true); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_CONTACTS); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSaveOnClick_callsUncheck() { + int[] endState; + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + + // A calls setup should never set conversations settings. + // For KEY_NONE, "unchecking" still means "none". + nonePref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(endState[0]).isEqualTo(STATE_UNSET); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For KEY_ANY unchecking resets the state to "none". + anyPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + // For [starred] contacts, we should unset the priority senders, but not the conversations + starredPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + + contactsPref.setChecked(false); + endState = mCallsController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(endState[0]).isEqualTo(PEOPLE_TYPE_NONE); + assertThat(endState[1]).isEqualTo(CONVERSATION_SENDERS_UNSET); + } + + @Test + public void testSettingsToSave_messages_noChange() { + int[] savedSettings; + + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, true, true); + nonePref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, true, true); + anyPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, true, true); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, true, true); + starredPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + contactsPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference impPref = makePreference(KEY_IMPORTANT, true, true); + impPref.setChecked(true); + savedSettings = mMessagesController.settingsToSaveOnClick( + impPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_IMPORTANT); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + } + + @Test + public void testSettingsToSave_calls_noChange() { + int[] savedSettings; + SelectorWithWidgetPreference nonePref = makePreference(KEY_NONE, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + nonePref, PEOPLE_TYPE_NONE, CONVERSATION_SENDERS_NONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference anyPref = makePreference(KEY_ANY, false, false); + + savedSettings = mMessagesController.settingsToSaveOnClick( + anyPref, PEOPLE_TYPE_ANYONE, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + assertThat(savedSettings[1]).isEqualTo(STATE_UNSET); + + SelectorWithWidgetPreference starredPref = makePreference(KEY_STARRED, false, false); + SelectorWithWidgetPreference contactsPref = makePreference(KEY_CONTACTS, false, false); + savedSettings = mMessagesController.settingsToSaveOnClick( + starredPref, PEOPLE_TYPE_STARRED, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + + savedSettings = mMessagesController.settingsToSaveOnClick( + contactsPref, PEOPLE_TYPE_CONTACTS, CONVERSATION_SENDERS_ANYONE); + assertThat(savedSettings[0]).isEqualTo(STATE_UNSET); + } +} \ No newline at end of file diff --git a/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java new file mode 100644 index 00000000000..621c5b06f9d --- /dev/null +++ b/tests/robotests/src/com/android/settings/notification/modes/ZenModesSummaryHelperTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.notification.modes; + +import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; +import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; +import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; +import static com.google.common.truth.Truth.assertThat; + +import android.app.AutomaticZenRule; +import android.content.Context; +import android.net.Uri; +import android.service.notification.ZenPolicy; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +@RunWith(RobolectricTestRunner.class) +public class ZenModesSummaryHelperTest { + private Context mContext; + private ZenModesBackend mBackend; + + private ZenModeSummaryHelper mSummaryHelper; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mBackend = new ZenModesBackend(mContext); + mSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); + } + + @Test + public void getPeopleSummary_noOne() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("No one can interrupt"); + } + + @Test + public void getPeopleSummary_some() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_CONTACTS).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("Some people can interrupt"); + } + + @Test + public void getPeopleSummary_all() { + AutomaticZenRule rule = new AutomaticZenRule.Builder("Bedtime", Uri.parse("bed")) + .setType(AutomaticZenRule.TYPE_BEDTIME) + .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) + .setZenPolicy(new ZenPolicy.Builder().allowCalls(PEOPLE_TYPE_ANYONE). + allowConversations(CONVERSATION_SENDERS_ANYONE) + .allowMessages(PEOPLE_TYPE_ANYONE).build()) + .build(); + ZenMode zenMode = new ZenMode("id", rule, true); + + assertThat(mSummaryHelper.getPeopleSummary(zenMode)).isEqualTo("All people can interrupt"); + } +}