/* * 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 static com.google.common.base.Preconditions.checkNotNull; import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.icu.text.MessageFormat; import android.os.UserHandle; import android.os.UserManager; import android.provider.Contacts; import android.service.notification.ZenPolicy; import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; 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.dashboard.profileselector.ProfileSelectDialog; import com.android.settings.notification.app.ConversationListSettings; import com.android.settingslib.notification.modes.ZenMode; import com.android.settingslib.notification.modes.ZenModesBackend; import com.android.settingslib.widget.SelectorWithWidgetPreference; import com.google.common.collect.ImmutableSet; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** * 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. */ 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 = "conversations_important"; static final String KEY_ANY_CONVERSATIONS = "conversations_any"; static final String KEY_NONE = "senders_none"; private int mNumAllConversations = 0; private int mNumImportantConversations = 0; 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_CONTACTS_INTENT = new Intent(Intent.ACTION_MAIN) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); private final ZenHelperBackend mHelperBackend; private final UserManager mUserManager; private final PackageManager mPackageManager; private PreferenceCategory mPreferenceCategory; private final LinkedHashMap mOptions = new LinkedHashMap<>(); private final ZenModeSummaryHelper mZenModeSummaryHelper; @Nullable private Dialog mProfileSelectDialog; public ZenModePrioritySendersPreferenceController(Context context, String key, boolean isMessages, ZenModesBackend backend, ZenHelperBackend helperBackend) { super(context, key, backend); mIsMessages = isMessages; mHelperBackend = helperBackend; String contactsPackage = context.getString(R.string.config_contacts_package_name); ALL_CONTACTS_INTENT.setPackage(contactsPackage); STARRED_CONTACTS_INTENT.setPackage(contactsPackage); FALLBACK_CONTACTS_INTENT.setPackage(contactsPackage); mUserManager = mContext.getSystemService(UserManager.class); mPackageManager = mContext.getPackageManager(); if (!FALLBACK_CONTACTS_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { FALLBACK_CONTACTS_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); } mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mHelperBackend); } @Override public void displayPreference(PreferenceScreen screen) { mPreferenceCategory = checkNotNull(screen.findPreference(getPreferenceKey())); if (mPreferenceCategory.getPreferenceCount() == 0) { makeSelectorPreference(KEY_STARRED, R.string.zen_mode_from_starred, R.string.zen_mode_from_starred_settings, mIsMessages, true); makeSelectorPreference(KEY_CONTACTS, R.string.zen_mode_from_contacts, R.string.zen_mode_from_contacts_settings, mIsMessages, true); if (mIsMessages) { // "Any conversations" will only be available as option if it is the current value. // Because it's confusing and we don't want users setting it up that way, but apps // could create such ZenPolicies and we must show that. makeSelectorPreference(KEY_ANY_CONVERSATIONS, R.string.zen_mode_from_all_conversations, R.string.zen_mode_from_conversations_settings, true, /* isVisibleByDefault= */ false); makeSelectorPreference(KEY_IMPORTANT_CONVERSATIONS, R.string.zen_mode_from_important_conversations, R.string.zen_mode_from_conversations_settings, true, true); } makeSelectorPreference(KEY_ANY, R.string.zen_mode_from_anyone, null, mIsMessages, true); makeSelectorPreference(KEY_NONE, R.string.zen_mode_none_messages, null, mIsMessages, true); } super.displayPreference(screen); } @Override public void updateState(Preference preference, @NonNull ZenMode zenMode) { final int contacts = getPrioritySenders(zenMode.getPolicy()); final int conversations = getPriorityConversationSenders(zenMode.getPolicy()); if (mIsMessages) { updateChannelCounts(); if (contacts == PEOPLE_TYPE_ANYONE) { setSelectedOption(KEY_ANY); } else if (contacts == PEOPLE_TYPE_NONE && conversations == CONVERSATION_SENDERS_NONE) { setSelectedOption(KEY_NONE); } else { ImmutableSet.Builder selectedOptions = new ImmutableSet.Builder<>(); if (contacts == PEOPLE_TYPE_STARRED) { selectedOptions.add(KEY_STARRED); } else if (contacts == PEOPLE_TYPE_CONTACTS) { selectedOptions.add(KEY_CONTACTS); } if (conversations == CONVERSATION_SENDERS_IMPORTANT) { selectedOptions.add(KEY_IMPORTANT_CONVERSATIONS); } else if (conversations == CONVERSATION_SENDERS_ANYONE) { selectedOptions.add(KEY_ANY_CONVERSATIONS); } setSelectedOptions(selectedOptions.build()); } } else { // Calls is easy! switch (contacts) { case PEOPLE_TYPE_ANYONE -> setSelectedOption(KEY_ANY); case PEOPLE_TYPE_CONTACTS -> setSelectedOption(KEY_CONTACTS); case PEOPLE_TYPE_STARRED -> setSelectedOption(KEY_STARRED); case PEOPLE_TYPE_NONE -> setSelectedOption(KEY_NONE); default -> throw new IllegalArgumentException("Unexpected PeopleType: " + contacts); } } updateSummaries(); } private void setSelectedOption(String key) { setSelectedOptions(ImmutableSet.of(key)); } private void setSelectedOptions(Set keys) { if (keys.isEmpty()) { throw new IllegalArgumentException("At least one option should be selected!"); } for (SelectorWithWidgetPreference optionPreference : mOptions.values()) { optionPreference.setChecked(keys.contains(optionPreference.getKey())); if (optionPreference.isChecked()) { // Ensure selected options are visible. This is to support "Any conversations" // which is only shown if the policy has Conversations=Anyone (and doesn't have // messages=Anyone), and then remains visible until the user exits the page // (so that toggling back and forth is possible without the option disappearing). optionPreference.setVisible(true); } } } public void onResume() { if (mIsMessages) { updateChannelCounts(); } updateSummaries(); } private void updateChannelCounts() { mNumAllConversations = mHelperBackend.getAllConversations().size(); mNumImportantConversations = mHelperBackend.getImportantConversations().size(); } private int getPrioritySenders(ZenPolicy policy) { if (mIsMessages) { return policy.getPriorityMessageSenders(); } else { return policy.getPriorityCallSenders(); } } private int getPriorityConversationSenders(ZenPolicy policy) { if (mIsMessages) { return policy.getPriorityConversationSenders(); } return CONVERSATION_SENDERS_UNSET; } private void makeSelectorPreference(String key, @StringRes int titleId, @Nullable @StringRes Integer settingsContentDescriptionResId, boolean isCheckbox, boolean isVisibleByDefault) { final SelectorWithWidgetPreference pref = new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); pref.setKey(key); pref.setTitle(titleId); pref.setOnClickListener(mSelectorClickListener); pref.setVisible(isVisibleByDefault); View.OnClickListener widgetClickListener = getWidgetClickListener(key); if (widgetClickListener != null) { pref.setExtraWidgetOnClickListener(widgetClickListener); pref.setExtraWidgetContentDescription(settingsContentDescriptionResId != null ? mContext.getString(settingsContentDescriptionResId) : null); } mPreferenceCategory.addPreference(pref); mOptions.put(key, pref); } private View.OnClickListener getWidgetClickListener(String key) { if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_ANY_CONVERSATIONS.equals(key) && !KEY_IMPORTANT_CONVERSATIONS.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)) { startContactsActivity(STARRED_CONTACTS_INTENT); } else if (KEY_CONTACTS.equals(key)) { startContactsActivity(ALL_CONTACTS_INTENT); } else if (KEY_ANY_CONVERSATIONS.equals(key) || KEY_IMPORTANT_CONVERSATIONS.equals(key)) { new SubSettingLauncher(mContext) .setDestination(ConversationListSettings.class.getName()) .setSourceMetricsCategory(SettingsEnums.DND_MESSAGES) .launch(); } }; } private void startContactsActivity(Intent preferredIntent) { Intent intent = preferredIntent.resolveActivity(mPackageManager) != null ? preferredIntent : FALLBACK_CONTACTS_INTENT; List userProfiles = mUserManager.getEnabledProfiles(); if (userProfiles.size() <= 1) { mContext.startActivity(intent); return; } mProfileSelectDialog = ProfileSelectDialog.createDialog(mContext, userProfiles, position -> { mContext.startActivityAsUser(intent, userProfiles.get(position)); if (mProfileSelectDialog != null) { mProfileSelectDialog.dismiss(); mProfileSelectDialog = null; } }); mProfileSelectDialog.show(); } private boolean isStarredIntentValid() { return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null; } private boolean isContactsIntentValid() { return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null; } void updateSummaries() { for (SelectorWithWidgetPreference pref : mOptions.values()) { 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. private 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/any conversations" and "any" should reset // conversation state to "NONE" as well. if (mIsMessages) { switch (key) { case KEY_IMPORTANT_CONVERSATIONS: case KEY_ANY_CONVERSATIONS: 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_CONVERSATIONS: endState[1] = CONVERSATION_SENDERS_IMPORTANT; break; case KEY_ANY_CONVERSATIONS: 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". private int[] settingsToSaveOnClick(String key, boolean checked, 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(key, checked); 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 ((key.equals(KEY_IMPORTANT_CONVERSATIONS) || key.equals(KEY_ANY_CONVERSATIONS)) && currSendersSetting == PEOPLE_TYPE_ANYONE) { savedSettings[0] = PEOPLE_TYPE_NONE; } // The flip-side case for the "contacts" option is slightly different -- we only // reset conversations if leaving PEOPLE_ANY by selecting a contact option, but not // if switching contact options. That's because starting from Anyone, checking Contacts, // and then "important conversations" also shown checked because it was there (albeit // subsumed into PEOPLE_ANY) would be weird. if ((key.equals(KEY_STARRED) || key.equals(KEY_CONTACTS)) && currSendersSetting == PEOPLE_TYPE_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_ANY_CONVERSATIONS: return getConversationSummary(mNumAllConversations); case KEY_IMPORTANT_CONVERSATIONS: return getConversationSummary(mNumImportantConversations); 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(int numConversations) { 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); } } private final SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = new SelectorWithWidgetPreference.OnClickListener() { @Override public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { savePolicy(policy -> { ZenPolicy previousPolicy = policy.build(); final int[] settingsToSave = settingsToSaveOnClick( preference.getKey(), preference.isCheckBox() ? !preference.isChecked() : true, getPrioritySenders(previousPolicy), getPriorityConversationSenders(previousPolicy)); final int prioritySendersSetting = settingsToSave[0]; final int priorityConvosSetting = settingsToSave[1]; if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { if (mIsMessages) { policy.allowMessages(prioritySendersSetting); } else { policy.allowCalls(prioritySendersSetting); } } if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { policy.allowConversations(priorityConvosSetting); } return policy; }); } }; }