1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.notification.modes; 18 19 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; 20 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT; 21 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE; 22 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_UNSET; 23 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE; 24 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS; 25 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE; 26 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; 27 import static android.service.notification.ZenPolicy.PEOPLE_TYPE_UNSET; 28 29 import static com.google.common.base.Preconditions.checkNotNull; 30 31 import android.app.Dialog; 32 import android.app.settings.SettingsEnums; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.pm.PackageManager; 36 import android.icu.text.MessageFormat; 37 import android.os.UserHandle; 38 import android.os.UserManager; 39 import android.provider.Contacts; 40 import android.service.notification.ZenPolicy; 41 import android.view.View; 42 43 import androidx.annotation.NonNull; 44 import androidx.annotation.Nullable; 45 import androidx.annotation.StringRes; 46 import androidx.preference.Preference; 47 import androidx.preference.PreferenceCategory; 48 import androidx.preference.PreferenceScreen; 49 50 import com.android.settings.R; 51 import com.android.settings.core.SubSettingLauncher; 52 import com.android.settings.dashboard.profileselector.ProfileSelectDialog; 53 import com.android.settings.notification.app.ConversationListSettings; 54 import com.android.settingslib.notification.modes.ZenMode; 55 import com.android.settingslib.notification.modes.ZenModesBackend; 56 import com.android.settingslib.widget.SelectorWithWidgetPreference; 57 58 import com.google.common.collect.ImmutableSet; 59 60 import java.util.HashMap; 61 import java.util.LinkedHashMap; 62 import java.util.List; 63 import java.util.Locale; 64 import java.util.Map; 65 import java.util.Set; 66 67 /** 68 * Common preference controller functionality for zen mode priority senders preferences for both 69 * messages and calls. 70 * 71 * These controllers handle the settings regarding which priority senders that are allowed to 72 * bypass DND for calls or messages, which may be one of the following values: starred contacts, all 73 * contacts, priority conversations (for messages only), anyone, or no one. 74 */ 75 class ZenModePrioritySendersPreferenceController 76 extends AbstractZenModePreferenceController { 77 private final boolean mIsMessages; // if this is false, then this preference is for calls 78 79 static final String KEY_ANY = "senders_anyone"; 80 static final String KEY_CONTACTS = "senders_contacts"; 81 static final String KEY_STARRED = "senders_starred_contacts"; 82 static final String KEY_IMPORTANT_CONVERSATIONS = "conversations_important"; 83 static final String KEY_ANY_CONVERSATIONS = "conversations_any"; 84 static final String KEY_NONE = "senders_none"; 85 86 private int mNumAllConversations = 0; 87 private int mNumImportantConversations = 0; 88 89 private static final Intent ALL_CONTACTS_INTENT = 90 new Intent(Contacts.Intents.UI.LIST_DEFAULT) 91 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 92 private static final Intent STARRED_CONTACTS_INTENT = 93 new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) 94 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 95 private static final Intent FALLBACK_CONTACTS_INTENT = new Intent(Intent.ACTION_MAIN) 96 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 97 98 private final ZenHelperBackend mHelperBackend; 99 private final UserManager mUserManager; 100 private final PackageManager mPackageManager; 101 private PreferenceCategory mPreferenceCategory; 102 private final LinkedHashMap<String, SelectorWithWidgetPreference> mOptions = 103 new LinkedHashMap<>(); 104 105 private final ZenModeSummaryHelper mZenModeSummaryHelper; 106 @Nullable private Dialog mProfileSelectDialog; 107 ZenModePrioritySendersPreferenceController(Context context, String key, boolean isMessages, ZenModesBackend backend, ZenHelperBackend helperBackend)108 public ZenModePrioritySendersPreferenceController(Context context, String key, 109 boolean isMessages, ZenModesBackend backend, ZenHelperBackend helperBackend) { 110 super(context, key, backend); 111 mIsMessages = isMessages; 112 mHelperBackend = helperBackend; 113 114 String contactsPackage = context.getString(R.string.config_contacts_package_name); 115 ALL_CONTACTS_INTENT.setPackage(contactsPackage); 116 STARRED_CONTACTS_INTENT.setPackage(contactsPackage); 117 FALLBACK_CONTACTS_INTENT.setPackage(contactsPackage); 118 119 mUserManager = mContext.getSystemService(UserManager.class); 120 mPackageManager = mContext.getPackageManager(); 121 if (!FALLBACK_CONTACTS_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { 122 FALLBACK_CONTACTS_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); 123 } 124 mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mHelperBackend); 125 } 126 127 @Override displayPreference(PreferenceScreen screen)128 public void displayPreference(PreferenceScreen screen) { 129 mPreferenceCategory = checkNotNull(screen.findPreference(getPreferenceKey())); 130 if (mPreferenceCategory.getPreferenceCount() == 0) { 131 makeSelectorPreference(KEY_STARRED, R.string.zen_mode_from_starred, 132 R.string.zen_mode_from_starred_settings, mIsMessages, true); 133 makeSelectorPreference(KEY_CONTACTS, R.string.zen_mode_from_contacts, 134 R.string.zen_mode_from_contacts_settings, mIsMessages, true); 135 if (mIsMessages) { 136 // "Any conversations" will only be available as option if it is the current value. 137 // Because it's confusing and we don't want users setting it up that way, but apps 138 // could create such ZenPolicies and we must show that. 139 makeSelectorPreference(KEY_ANY_CONVERSATIONS, 140 R.string.zen_mode_from_all_conversations, 141 R.string.zen_mode_from_conversations_settings, true, 142 /* isVisibleByDefault= */ false); 143 makeSelectorPreference(KEY_IMPORTANT_CONVERSATIONS, 144 R.string.zen_mode_from_important_conversations, 145 R.string.zen_mode_from_conversations_settings, true, 146 true); 147 } 148 makeSelectorPreference(KEY_ANY, 149 R.string.zen_mode_from_anyone, null, mIsMessages, true); 150 makeSelectorPreference(KEY_NONE, 151 mIsMessages ? R.string.zen_mode_none_messages : R.string.zen_mode_none_calls, 152 null, mIsMessages, true); 153 } 154 super.displayPreference(screen); 155 } 156 157 @Override updateState(Preference preference, @NonNull ZenMode zenMode)158 public void updateState(Preference preference, @NonNull ZenMode zenMode) { 159 final int contacts = getPrioritySenders(zenMode.getPolicy()); 160 final int conversations = getPriorityConversationSenders(zenMode.getPolicy()); 161 162 if (mIsMessages) { 163 updateChannelCounts(); 164 165 if (contacts == PEOPLE_TYPE_ANYONE) { 166 setSelectedOption(KEY_ANY); 167 } else if (contacts == PEOPLE_TYPE_NONE && conversations == CONVERSATION_SENDERS_NONE) { 168 setSelectedOption(KEY_NONE); 169 } else { 170 ImmutableSet.Builder<String> selectedOptions = new ImmutableSet.Builder<>(); 171 if (contacts == PEOPLE_TYPE_STARRED) { 172 selectedOptions.add(KEY_STARRED); 173 } else if (contacts == PEOPLE_TYPE_CONTACTS) { 174 selectedOptions.add(KEY_CONTACTS); 175 } 176 if (conversations == CONVERSATION_SENDERS_IMPORTANT) { 177 selectedOptions.add(KEY_IMPORTANT_CONVERSATIONS); 178 } else if (conversations == CONVERSATION_SENDERS_ANYONE) { 179 selectedOptions.add(KEY_ANY_CONVERSATIONS); 180 } 181 setSelectedOptions(selectedOptions.build()); 182 } 183 } else { 184 // Calls is easy! 185 switch (contacts) { 186 case PEOPLE_TYPE_ANYONE -> setSelectedOption(KEY_ANY); 187 case PEOPLE_TYPE_CONTACTS -> setSelectedOption(KEY_CONTACTS); 188 case PEOPLE_TYPE_STARRED -> setSelectedOption(KEY_STARRED); 189 case PEOPLE_TYPE_NONE -> setSelectedOption(KEY_NONE); 190 default -> throw new IllegalArgumentException("Unexpected PeopleType: " + contacts); 191 } 192 } 193 194 updateSummaries(); 195 } 196 setSelectedOption(String key)197 private void setSelectedOption(String key) { 198 setSelectedOptions(ImmutableSet.of(key)); 199 } 200 setSelectedOptions(Set<String> keys)201 private void setSelectedOptions(Set<String> keys) { 202 if (keys.isEmpty()) { 203 throw new IllegalArgumentException("At least one option should be selected!"); 204 } 205 206 for (SelectorWithWidgetPreference optionPreference : mOptions.values()) { 207 optionPreference.setChecked(keys.contains(optionPreference.getKey())); 208 if (optionPreference.isChecked()) { 209 // Ensure selected options are visible. This is to support "Any conversations" 210 // which is only shown if the policy has Conversations=Anyone (and doesn't have 211 // messages=Anyone), and then remains visible until the user exits the page 212 // (so that toggling back and forth is possible without the option disappearing). 213 optionPreference.setVisible(true); 214 } 215 } 216 } 217 onResume()218 public void onResume() { 219 if (mIsMessages) { 220 updateChannelCounts(); 221 } 222 updateSummaries(); 223 } 224 updateChannelCounts()225 private void updateChannelCounts() { 226 mNumAllConversations = mHelperBackend.getAllConversations().size(); 227 mNumImportantConversations = mHelperBackend.getImportantConversations().size(); 228 } 229 getPrioritySenders(ZenPolicy policy)230 private int getPrioritySenders(ZenPolicy policy) { 231 if (mIsMessages) { 232 return policy.getPriorityMessageSenders(); 233 } else { 234 return policy.getPriorityCallSenders(); 235 } 236 } 237 getPriorityConversationSenders(ZenPolicy policy)238 private int getPriorityConversationSenders(ZenPolicy policy) { 239 if (mIsMessages) { 240 return policy.getPriorityConversationSenders(); 241 } 242 return CONVERSATION_SENDERS_UNSET; 243 } 244 makeSelectorPreference(String key, @StringRes int titleId, @Nullable @StringRes Integer settingsContentDescriptionResId, boolean isCheckbox, boolean isVisibleByDefault)245 private void makeSelectorPreference(String key, @StringRes int titleId, 246 @Nullable @StringRes Integer settingsContentDescriptionResId, boolean isCheckbox, 247 boolean isVisibleByDefault) { 248 final SelectorWithWidgetPreference pref = 249 new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); 250 pref.setKey(key); 251 pref.setTitle(titleId); 252 pref.setOnClickListener(mSelectorClickListener); 253 pref.setVisible(isVisibleByDefault); 254 255 View.OnClickListener widgetClickListener = getWidgetClickListener(key); 256 if (widgetClickListener != null) { 257 pref.setExtraWidgetOnClickListener(widgetClickListener); 258 pref.setExtraWidgetContentDescription(settingsContentDescriptionResId != null 259 ? mContext.getString(settingsContentDescriptionResId) 260 : null); 261 } 262 263 mPreferenceCategory.addPreference(pref); 264 mOptions.put(key, pref); 265 } 266 getWidgetClickListener(String key)267 private View.OnClickListener getWidgetClickListener(String key) { 268 if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) 269 && !KEY_ANY_CONVERSATIONS.equals(key) && !KEY_IMPORTANT_CONVERSATIONS.equals(key)) { 270 return null; 271 } 272 273 if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { 274 return null; 275 } 276 277 if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { 278 return null; 279 } 280 281 return v -> { 282 if (KEY_STARRED.equals(key)) { 283 startContactsActivity(STARRED_CONTACTS_INTENT); 284 } else if (KEY_CONTACTS.equals(key)) { 285 startContactsActivity(ALL_CONTACTS_INTENT); 286 } else if (KEY_ANY_CONVERSATIONS.equals(key) 287 || KEY_IMPORTANT_CONVERSATIONS.equals(key)) { 288 new SubSettingLauncher(mContext) 289 .setDestination(ConversationListSettings.class.getName()) 290 .setSourceMetricsCategory(SettingsEnums.DND_MESSAGES) 291 .launch(); 292 } 293 }; 294 } 295 296 private void startContactsActivity(Intent preferredIntent) { 297 Intent intent = preferredIntent.resolveActivity(mPackageManager) != null 298 ? preferredIntent : FALLBACK_CONTACTS_INTENT; 299 300 List<UserHandle> userProfiles = mUserManager.getEnabledProfiles(); 301 if (userProfiles.size() <= 1) { 302 mContext.startActivity(intent); 303 return; 304 } 305 306 mProfileSelectDialog = ProfileSelectDialog.createDialog(mContext, userProfiles, 307 position -> { 308 mContext.startActivityAsUser(intent, userProfiles.get(position)); 309 if (mProfileSelectDialog != null) { 310 mProfileSelectDialog.dismiss(); 311 mProfileSelectDialog = null; 312 } 313 }); 314 mProfileSelectDialog.show(); 315 } 316 317 private boolean isStarredIntentValid() { 318 return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 319 || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null; 320 } 321 322 private boolean isContactsIntentValid() { 323 return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 324 || FALLBACK_CONTACTS_INTENT.resolveActivity(mPackageManager) != null; 325 } 326 327 void updateSummaries() { 328 for (SelectorWithWidgetPreference pref : mOptions.values()) { 329 pref.setSummary(getSummary(pref.getKey())); 330 } 331 } 332 333 // Gets the desired end state of the priority senders and conversations for the given key 334 // and whether it is being checked or unchecked. [type]_UNSET indicates no change in state. 335 // 336 // Returns an integer array with 2 entries. The first entry is the setting for priority senders 337 // and the second entry is for priority conversation senders; if isMessages is false, then 338 // no changes will ever be prescribed for conversation senders. 339 private int[] keyToSettingEndState(String key, boolean checked) { 340 int[] endState = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; 341 if (!checked) { 342 // Unchecking any priority-senders-based state should reset the state to NONE. 343 // "Unchecking" the NONE state doesn't do anything, in practice. 344 switch (key) { 345 case KEY_STARRED: 346 case KEY_CONTACTS: 347 case KEY_ANY: 348 case KEY_NONE: 349 endState[0] = PEOPLE_TYPE_NONE; 350 } 351 352 // For messages, unchecking "priority/any conversations" and "any" should reset 353 // conversation state to "NONE" as well. 354 if (mIsMessages) { 355 switch (key) { 356 case KEY_IMPORTANT_CONVERSATIONS: 357 case KEY_ANY_CONVERSATIONS: 358 case KEY_ANY: 359 case KEY_NONE: 360 endState[1] = CONVERSATION_SENDERS_NONE; 361 } 362 } 363 } else { 364 // All below is for the enabling (checked) state. 365 switch (key) { 366 case KEY_STARRED: 367 endState[0] = PEOPLE_TYPE_STARRED; 368 break; 369 case KEY_CONTACTS: 370 endState[0] = PEOPLE_TYPE_CONTACTS; 371 break; 372 case KEY_ANY: 373 endState[0] = PEOPLE_TYPE_ANYONE; 374 break; 375 case KEY_NONE: 376 endState[0] = PEOPLE_TYPE_NONE; 377 } 378 379 // In the messages case *only*, also handle changing of conversation settings. 380 if (mIsMessages) { 381 switch (key) { 382 case KEY_IMPORTANT_CONVERSATIONS: 383 endState[1] = CONVERSATION_SENDERS_IMPORTANT; 384 break; 385 case KEY_ANY_CONVERSATIONS: 386 case KEY_ANY: 387 endState[1] = CONVERSATION_SENDERS_ANYONE; 388 break; 389 case KEY_NONE: 390 endState[1] = CONVERSATION_SENDERS_NONE; 391 } 392 } 393 } 394 // Error case check: if somehow, after all of that, endState is still 395 // {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong. 396 if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) { 397 throw new IllegalArgumentException("invalid key " + key); 398 } 399 400 return endState; 401 } 402 403 // Returns the preferences, if any, that should be newly saved for the specified setting and 404 // checked state in an array where index 0 is the new senders setting and 1 the new 405 // conversations setting. A return value of [type]_UNSET indicates that nothing should 406 // change. 407 // 408 // The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (not to change) 409 // in the calls case. 410 // 411 // Checking and unchecking is mostly an operation of setting or unsetting the relevant 412 // preference, except for some special handling where the conversation setting overlaps: 413 // - setting or unsetting "priority contacts" or "contacts" has no effect on the 414 // priority conversation setting, and vice versa 415 // - if "priority conversations" is selected, and the user checks "anyone", the conversation 416 // setting is also set to any conversations 417 // - if "anyone" is previously selected, and the user clicks "priority conversations", then 418 // the contacts setting is additionally reset to "none". 419 // - if "anyone" is previously selected, and the user clicks one of the contacts values, 420 // then the conversations setting is additionally reset to "none". 421 private int[] settingsToSaveOnClick(String key, boolean checked, 422 int currSendersSetting, int currConvosSetting) { 423 int[] savedSettings = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; 424 425 // If the preference isn't a checkbox, always consider this to be "checking" the setting. 426 // Otherwise, toggle. 427 final int[] endState = keyToSettingEndState(key, checked); 428 final int prioritySendersSetting = endState[0]; 429 final int priorityConvosSetting = endState[1]; 430 if (prioritySendersSetting != PEOPLE_TYPE_UNSET 431 && prioritySendersSetting != currSendersSetting) { 432 savedSettings[0] = prioritySendersSetting; 433 } 434 435 // Only handle conversation settings for the messages case. If not messages, there should 436 // never be any change to the conversation senders setting. 437 if (mIsMessages) { 438 if (priorityConvosSetting != CONVERSATION_SENDERS_UNSET 439 && priorityConvosSetting != currConvosSetting) { 440 savedSettings[1] = priorityConvosSetting; 441 } 442 443 // Special-case handling for the "priority conversations" checkbox: 444 // If a specific selection exists for priority senders (starred, contacts), we leave 445 // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. 446 if ((key.equals(KEY_IMPORTANT_CONVERSATIONS) || key.equals(KEY_ANY_CONVERSATIONS)) 447 && currSendersSetting == PEOPLE_TYPE_ANYONE) { 448 savedSettings[0] = PEOPLE_TYPE_NONE; 449 } 450 451 // The flip-side case for the "contacts" option is slightly different -- we only 452 // reset conversations if leaving PEOPLE_ANY by selecting a contact option, but not 453 // if switching contact options. That's because starting from Anyone, checking Contacts, 454 // and then "important conversations" also shown checked because it was there (albeit 455 // subsumed into PEOPLE_ANY) would be weird. 456 if ((key.equals(KEY_STARRED) || key.equals(KEY_CONTACTS)) 457 && currSendersSetting == PEOPLE_TYPE_ANYONE) { 458 savedSettings[1] = CONVERSATION_SENDERS_NONE; 459 } 460 } 461 462 return savedSettings; 463 } 464 465 private String getSummary(String key) { 466 switch (key) { 467 case KEY_STARRED: 468 return mZenModeSummaryHelper.getStarredContactsSummary(); 469 case KEY_CONTACTS: 470 return mZenModeSummaryHelper.getContactsNumberSummary(); 471 case KEY_ANY_CONVERSATIONS: 472 return getConversationSummary(mNumAllConversations); 473 case KEY_IMPORTANT_CONVERSATIONS: 474 return getConversationSummary(mNumImportantConversations); 475 case KEY_ANY: 476 return mContext.getResources().getString(mIsMessages 477 ? R.string.zen_mode_all_messages_summary 478 : R.string.zen_mode_all_calls_summary); 479 case KEY_NONE: 480 default: 481 return null; 482 } 483 } 484 485 private String getConversationSummary(int numConversations) { 486 if (numConversations == CONVERSATION_SENDERS_UNSET) { 487 return null; 488 } else { 489 MessageFormat msgFormat = new MessageFormat( 490 mContext.getString(R.string.zen_mode_conversations_count), 491 Locale.getDefault()); 492 Map<String, Object> args = new HashMap<>(); 493 args.put("count", numConversations); 494 return msgFormat.format(args); 495 } 496 } 497 498 private final SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = 499 new SelectorWithWidgetPreference.OnClickListener() { 500 @Override 501 public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { 502 savePolicy(policy -> { 503 ZenPolicy previousPolicy = policy.build(); 504 final int[] settingsToSave = settingsToSaveOnClick( 505 preference.getKey(), 506 preference.isCheckBox() ? !preference.isChecked() : true, 507 getPrioritySenders(previousPolicy), 508 getPriorityConversationSenders(previousPolicy)); 509 final int prioritySendersSetting = settingsToSave[0]; 510 final int priorityConvosSetting = settingsToSave[1]; 511 512 if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { 513 if (mIsMessages) { 514 policy.allowMessages(prioritySendersSetting); 515 } else { 516 policy.allowCalls(prioritySendersSetting); 517 } 518 } 519 if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { 520 policy.allowConversations(priorityConvosSetting); 521 } 522 return policy; 523 }); 524 } 525 }; 526 } 527