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 android.app.settings.SettingsEnums; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ParceledListSlice; 34 import android.icu.text.MessageFormat; 35 import android.provider.Contacts; 36 import android.service.notification.ConversationChannelWrapper; 37 import android.service.notification.ZenPolicy; 38 import android.view.View; 39 40 import androidx.annotation.NonNull; 41 import androidx.annotation.VisibleForTesting; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceCategory; 44 import androidx.preference.PreferenceScreen; 45 46 import com.android.settings.R; 47 import com.android.settings.core.SubSettingLauncher; 48 import com.android.settings.notification.app.ConversationListSettings; 49 import com.android.settingslib.widget.SelectorWithWidgetPreference; 50 51 import java.util.ArrayList; 52 import java.util.HashMap; 53 import java.util.List; 54 import java.util.Locale; 55 import java.util.Map; 56 57 /** 58 * Common preference controller functionality for zen mode priority senders preferences for both 59 * messages and calls. 60 * 61 * These controllers handle the settings regarding which priority senders that are allowed to 62 * bypass DND for calls or messages, which may be one of the following values: starred contacts, all 63 * contacts, priority conversations (for messages only), anyone, or no one. 64 */ 65 class ZenModePrioritySendersPreferenceController 66 extends AbstractZenModePreferenceController { 67 private final boolean mIsMessages; // if this is false, then this preference is for calls 68 69 static final String KEY_ANY = "senders_anyone"; 70 static final String KEY_CONTACTS = "senders_contacts"; 71 static final String KEY_STARRED = "senders_starred_contacts"; 72 static final String KEY_IMPORTANT = "conversations_important"; 73 static final String KEY_NONE = "senders_none"; 74 75 private int mNumImportantConversations = 0; 76 77 private static final Intent ALL_CONTACTS_INTENT = 78 new Intent(Contacts.Intents.UI.LIST_DEFAULT) 79 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 80 private static final Intent STARRED_CONTACTS_INTENT = 81 new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) 82 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 83 private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) 84 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 85 86 private final PackageManager mPackageManager; 87 private PreferenceCategory mPreferenceCategory; 88 private List<SelectorWithWidgetPreference> mSelectorPreferences = new ArrayList<>(); 89 90 private final ZenModeSummaryHelper mZenModeSummaryHelper; 91 ZenModePrioritySendersPreferenceController(Context context, String key, boolean isMessages, ZenModesBackend backend)92 public ZenModePrioritySendersPreferenceController(Context context, String key, 93 boolean isMessages, ZenModesBackend backend) { 94 super(context, key, backend); 95 mIsMessages = isMessages; 96 97 String contactsPackage = context.getString(R.string.config_contacts_package_name); 98 ALL_CONTACTS_INTENT.setPackage(contactsPackage); 99 STARRED_CONTACTS_INTENT.setPackage(contactsPackage); 100 FALLBACK_INTENT.setPackage(contactsPackage); 101 102 mPackageManager = mContext.getPackageManager(); 103 if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { 104 FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); 105 } 106 mZenModeSummaryHelper = new ZenModeSummaryHelper(mContext, mBackend); 107 } 108 109 @Override displayPreference(PreferenceScreen screen)110 public void displayPreference(PreferenceScreen screen) { 111 mPreferenceCategory = screen.findPreference(getPreferenceKey()); 112 if (mPreferenceCategory.getPreferenceCount() == 0) { 113 makeSelectorPreference(KEY_STARRED, 114 com.android.settings.R.string.zen_mode_from_starred, mIsMessages); 115 makeSelectorPreference(KEY_CONTACTS, 116 com.android.settings.R.string.zen_mode_from_contacts, mIsMessages); 117 if (mIsMessages) { 118 makeSelectorPreference(KEY_IMPORTANT, 119 com.android.settings.R.string.zen_mode_from_important_conversations, true); 120 } 121 makeSelectorPreference(KEY_ANY, 122 com.android.settings.R.string.zen_mode_from_anyone, mIsMessages); 123 makeSelectorPreference(KEY_NONE, 124 com.android.settings.R.string.zen_mode_none_messages, mIsMessages); 125 } 126 super.displayPreference(screen); 127 } 128 129 @Override updateState(Preference preference, @NonNull ZenMode zenMode)130 public void updateState(Preference preference, @NonNull ZenMode zenMode) { 131 if (mIsMessages) { 132 updateChannelCounts(); 133 } 134 final int currContactsSetting = getPrioritySenders(zenMode.getPolicy()); 135 final int currConversationsSetting = getPriorityConversationSenders(zenMode.getPolicy()); 136 for (SelectorWithWidgetPreference pref : mSelectorPreferences) { 137 // for each preference, check whether the current state matches what this state 138 // would look like if the button were checked. 139 final int[] checkedState = keyToSettingEndState(pref.getKey(), true); 140 final int checkedContactsSetting = checkedState[0]; 141 final int checkedConversationsSetting = checkedState[1]; 142 143 boolean match = checkedContactsSetting == currContactsSetting; 144 if (mIsMessages && checkedConversationsSetting != CONVERSATION_SENDERS_UNSET) { 145 // "CONVERSATION_SENDERS_UNSET" in checkedContactsSetting means this preference 146 // doesn't govern the priority senders setting, so the full match happens when 147 // either the priority senders setting matches or if it's CONVERSATION_SENDERS_UNSET 148 // so only the conversation setting needs to match. 149 match = (match || checkedContactsSetting == PEOPLE_TYPE_UNSET) 150 && (checkedConversationsSetting == currConversationsSetting); 151 } 152 153 pref.setChecked(match); 154 } 155 updateSummaries(); 156 } 157 onResume()158 public void onResume() { 159 if (mIsMessages) { 160 updateChannelCounts(); 161 } 162 updateSummaries(); 163 } 164 updateChannelCounts()165 private void updateChannelCounts() { 166 ParceledListSlice<ConversationChannelWrapper> impConversations = 167 mBackend.getConversations(true); 168 int numImportantConversations = 0; 169 if (impConversations != null) { 170 for (ConversationChannelWrapper conversation : impConversations.getList()) { 171 if (!conversation.getNotificationChannel().isDemoted()) { 172 numImportantConversations++; 173 } 174 } 175 } 176 mNumImportantConversations = numImportantConversations; 177 } 178 getPrioritySenders(ZenPolicy policy)179 private int getPrioritySenders(ZenPolicy policy) { 180 if (mIsMessages) { 181 return policy.getPriorityMessageSenders(); 182 } else { 183 return policy.getPriorityCallSenders(); 184 } 185 } 186 getPriorityConversationSenders(ZenPolicy policy)187 private int getPriorityConversationSenders(ZenPolicy policy) { 188 if (mIsMessages) { 189 return policy.getPriorityConversationSenders(); 190 } 191 return CONVERSATION_SENDERS_UNSET; 192 } 193 makeSelectorPreference(String key, int titleId, boolean isCheckbox)194 private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId, 195 boolean isCheckbox) { 196 final SelectorWithWidgetPreference pref = 197 new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); 198 pref.setKey(key); 199 pref.setTitle(titleId); 200 pref.setOnClickListener(mSelectorClickListener); 201 202 View.OnClickListener widgetClickListener = getWidgetClickListener(key); 203 if (widgetClickListener != null) { 204 pref.setExtraWidgetOnClickListener(widgetClickListener); 205 } 206 207 mPreferenceCategory.addPreference(pref); 208 mSelectorPreferences.add(pref); 209 return pref; 210 } 211 getWidgetClickListener(String key)212 private View.OnClickListener getWidgetClickListener(String key) { 213 if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) { 214 return null; 215 } 216 217 if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { 218 return null; 219 } 220 221 if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { 222 return null; 223 } 224 225 return v -> { 226 if (KEY_STARRED.equals(key) 227 && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { 228 mContext.startActivity(STARRED_CONTACTS_INTENT); 229 } else if (KEY_CONTACTS.equals(key) 230 && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { 231 mContext.startActivity(ALL_CONTACTS_INTENT); 232 } else if (KEY_IMPORTANT.equals(key)) { 233 // TODO: b/332937635 - set correct metrics category 234 new SubSettingLauncher(mContext) 235 .setDestination(ConversationListSettings.class.getName()) 236 .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) 237 .launch(); 238 } else { 239 mContext.startActivity(FALLBACK_INTENT); 240 } 241 }; 242 } 243 244 private boolean isStarredIntentValid() { 245 return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 246 || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; 247 } 248 249 private boolean isContactsIntentValid() { 250 return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 251 || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; 252 } 253 254 void updateSummaries() { 255 for (SelectorWithWidgetPreference pref : mSelectorPreferences) { 256 pref.setSummary(getSummary(pref.getKey())); 257 } 258 } 259 260 // Gets the desired end state of the priority senders and conversations for the given key 261 // and whether it is being checked or unchecked. [type]_UNSET indicates no change in state. 262 // 263 // Returns an integer array with 2 entries. The first entry is the setting for priority senders 264 // and the second entry is for priority conversation senders; if isMessages is false, then 265 // no changes will ever be prescribed for conversation senders. 266 int[] keyToSettingEndState(String key, boolean checked) { 267 int[] endState = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; 268 if (!checked) { 269 // Unchecking any priority-senders-based state should reset the state to NONE. 270 // "Unchecking" the NONE state doesn't do anything, in practice. 271 switch (key) { 272 case KEY_STARRED: 273 case KEY_CONTACTS: 274 case KEY_ANY: 275 case KEY_NONE: 276 endState[0] = PEOPLE_TYPE_NONE; 277 } 278 279 // For messages, unchecking "priority conversations" and "any" should reset conversation 280 // state to "NONE" as well. 281 if (mIsMessages) { 282 switch (key) { 283 case KEY_IMPORTANT: 284 case KEY_ANY: 285 case KEY_NONE: 286 endState[1] = CONVERSATION_SENDERS_NONE; 287 } 288 } 289 } else { 290 // All below is for the enabling (checked) state. 291 switch (key) { 292 case KEY_STARRED: 293 endState[0] = PEOPLE_TYPE_STARRED; 294 break; 295 case KEY_CONTACTS: 296 endState[0] = PEOPLE_TYPE_CONTACTS; 297 break; 298 case KEY_ANY: 299 endState[0] = PEOPLE_TYPE_ANYONE; 300 break; 301 case KEY_NONE: 302 endState[0] = PEOPLE_TYPE_NONE; 303 } 304 305 // In the messages case *only*, also handle changing of conversation settings. 306 if (mIsMessages) { 307 switch (key) { 308 case KEY_IMPORTANT: 309 endState[1] = CONVERSATION_SENDERS_IMPORTANT; 310 break; 311 case KEY_ANY: 312 endState[1] = CONVERSATION_SENDERS_ANYONE; 313 break; 314 case KEY_NONE: 315 endState[1] = CONVERSATION_SENDERS_NONE; 316 } 317 } 318 } 319 // Error case check: if somehow, after all of that, endState is still 320 // {PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET}, something has gone wrong. 321 if (endState[0] == PEOPLE_TYPE_UNSET && endState[1] == CONVERSATION_SENDERS_UNSET) { 322 throw new IllegalArgumentException("invalid key " + key); 323 } 324 325 return endState; 326 } 327 328 // Returns the preferences, if any, that should be newly saved for the specified setting and 329 // checked state in an array where index 0 is the new senders setting and 1 the new 330 // conversations setting. A return value of [type]_UNSET indicates that nothing should 331 // change. 332 // 333 // The returned conversations setting will always be CONVERSATION_SENDERS_UNSET (not to change) 334 // in the calls case. 335 // 336 // Checking and unchecking is mostly an operation of setting or unsetting the relevant 337 // preference, except for some special handling where the conversation setting overlaps: 338 // - setting or unsetting "priority contacts" or "contacts" has no effect on the 339 // priority conversation setting, and vice versa 340 // - if "priority conversations" is selected, and the user checks "anyone", the conversation 341 // setting is also set to any conversations 342 // - if "anyone" is previously selected, and the user clicks "priority conversations", then 343 // the contacts setting is additionally reset to "none". 344 // - if "anyone" is previously selected, and the user clicks one of the contacts values, 345 // then the conversations setting is additionally reset to "none". 346 int[] settingsToSaveOnClick(String key, boolean checked, 347 int currSendersSetting, int currConvosSetting) { 348 int[] savedSettings = new int[]{ PEOPLE_TYPE_UNSET, CONVERSATION_SENDERS_UNSET }; 349 350 // If the preference isn't a checkbox, always consider this to be "checking" the setting. 351 // Otherwise, toggle. 352 final int[] endState = keyToSettingEndState(key, checked); 353 final int prioritySendersSetting = endState[0]; 354 final int priorityConvosSetting = endState[1]; 355 if (prioritySendersSetting != PEOPLE_TYPE_UNSET 356 && prioritySendersSetting != currSendersSetting) { 357 savedSettings[0] = prioritySendersSetting; 358 } 359 360 // Only handle conversation settings for the messages case. If not messages, there should 361 // never be any change to the conversation senders setting. 362 if (mIsMessages) { 363 if (priorityConvosSetting != CONVERSATION_SENDERS_UNSET 364 && priorityConvosSetting != currConvosSetting) { 365 savedSettings[1] = priorityConvosSetting; 366 } 367 368 // Special-case handling for the "priority conversations" checkbox: 369 // If a specific selection exists for priority senders (starred, contacts), we leave 370 // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. 371 if (key.equals(KEY_IMPORTANT) 372 && currSendersSetting == PEOPLE_TYPE_ANYONE) { 373 savedSettings[0] = PEOPLE_TYPE_NONE; 374 } 375 376 // Flip-side special case for clicking either "contacts" option: if a specific selection 377 // exists for priority conversations, leave it untouched; otherwise, set to none. 378 if ((key.equals(KEY_STARRED) || key.equals(KEY_CONTACTS)) 379 && currConvosSetting == CONVERSATION_SENDERS_ANYONE) { 380 savedSettings[1] = CONVERSATION_SENDERS_NONE; 381 } 382 } 383 384 return savedSettings; 385 } 386 387 private String getSummary(String key) { 388 switch (key) { 389 case KEY_STARRED: 390 return mZenModeSummaryHelper.getStarredContactsSummary(); 391 case KEY_CONTACTS: 392 return mZenModeSummaryHelper.getContactsNumberSummary(); 393 case KEY_IMPORTANT: 394 return getConversationSummary(); 395 case KEY_ANY: 396 return mContext.getResources().getString(mIsMessages 397 ? R.string.zen_mode_all_messages_summary 398 : R.string.zen_mode_all_calls_summary); 399 case KEY_NONE: 400 default: 401 return null; 402 } 403 } 404 405 private String getConversationSummary() { 406 final int numConversations = mNumImportantConversations; 407 408 if (numConversations == CONVERSATION_SENDERS_UNSET) { 409 return null; 410 } else { 411 MessageFormat msgFormat = new MessageFormat( 412 mContext.getString(R.string.zen_mode_conversations_count), 413 Locale.getDefault()); 414 Map<String, Object> args = new HashMap<>(); 415 args.put("count", numConversations); 416 return msgFormat.format(args); 417 } 418 } 419 420 @VisibleForTesting 421 SelectorWithWidgetPreference.OnClickListener mSelectorClickListener = 422 new SelectorWithWidgetPreference.OnClickListener() { 423 @Override 424 public void onRadioButtonClicked(SelectorWithWidgetPreference preference) { 425 savePolicy(policy -> { 426 ZenPolicy previousPolicy = policy.build(); 427 final int[] settingsToSave = settingsToSaveOnClick( 428 preference.getKey(), 429 preference.isCheckBox() ? !preference.isChecked() : true, 430 getPrioritySenders(previousPolicy), 431 getPriorityConversationSenders(previousPolicy)); 432 final int prioritySendersSetting = settingsToSave[0]; 433 final int priorityConvosSetting = settingsToSave[1]; 434 435 if (prioritySendersSetting != PEOPLE_TYPE_UNSET) { 436 if (mIsMessages) { 437 policy.allowMessages(prioritySendersSetting); 438 } else { 439 policy.allowCalls(prioritySendersSetting); 440 } 441 } 442 if (mIsMessages && priorityConvosSetting != CONVERSATION_SENDERS_UNSET) { 443 policy.allowConversations(priorityConvosSetting); 444 } 445 return policy; 446 }); 447 } 448 }; 449 } 450