Files
app_Settings/src/com/android/settings/notification/modes/ZenModePrioritySendersPreferenceController.java
Matías Hernández b152c2d34f Fix stray ProfileSelectDialog when only one profile exists
Fixes: 377829434
Test: atest ZenModePrioritySendersPreferenceControllerTest
Flag: android.app.modes_ui
Change-Id: Ia5f667fd937107af98dfb3a073cadeb935ff8a8a
2024-11-07 18:52:59 +01:00

526 lines
24 KiB
Java

/*
* 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<String, SelectorWithWidgetPreference> 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<String> 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<String> 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<UserHandle> 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<String, Object> 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;
});
}
};
}