/* * 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; 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); } } 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; } }