1 /* 2 * Copyright (C) 2021 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.zen; 18 19 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE; 20 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT; 21 import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE; 22 import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY; 23 24 import android.app.NotificationManager; 25 import android.app.settings.SettingsEnums; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.pm.ParceledListSlice; 30 import android.icu.text.MessageFormat; 31 import android.provider.Contacts; 32 import android.service.notification.ConversationChannelWrapper; 33 import android.view.View; 34 35 import androidx.preference.PreferenceCategory; 36 37 import com.android.settings.R; 38 import com.android.settings.core.SubSettingLauncher; 39 import com.android.settings.notification.NotificationBackend; 40 import com.android.settings.notification.app.ConversationListSettings; 41 import com.android.settingslib.widget.SelectorWithWidgetPreference; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.Map; 48 49 /** 50 * Shared class implementing priority senders logic to be used both for zen mode and zen custom 51 * rules, governing which senders can break through DND. This helper class controls creating 52 * and displaying the relevant preferences for either messages or calls mode, and determining 53 * what the priority and conversation senders settings should be given a click. 54 * 55 * The outer classes govern how those settings are stored -- for instance, where and how they 56 * are saved, and where they're read from to get current status. 57 */ 58 public class ZenPrioritySendersHelper { 59 public static final String TAG = "ZenPrioritySendersHelper"; 60 61 static final int UNKNOWN = -10; 62 static final String KEY_ANY = "senders_anyone"; 63 static final String KEY_CONTACTS = "senders_contacts"; 64 static final String KEY_STARRED = "senders_starred_contacts"; 65 static final String KEY_IMPORTANT = "conversations_important"; 66 static final String KEY_NONE = "senders_none"; 67 68 private int mNumImportantConversations = UNKNOWN; 69 70 private static final Intent ALL_CONTACTS_INTENT = 71 new Intent(Contacts.Intents.UI.LIST_DEFAULT) 72 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 73 private static final Intent STARRED_CONTACTS_INTENT = 74 new Intent(Contacts.Intents.UI.LIST_STARRED_ACTION) 75 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 76 private static final Intent FALLBACK_INTENT = new Intent(Intent.ACTION_MAIN) 77 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 78 79 private final Context mContext; 80 private final ZenModeBackend mZenModeBackend; 81 private final NotificationBackend mNotificationBackend; 82 private final PackageManager mPackageManager; 83 private final boolean mIsMessages; // if this is false, then this preference is for calls 84 private final SelectorWithWidgetPreference.OnClickListener mSelectorClickListener; 85 86 private PreferenceCategory mPreferenceCategory; 87 private List<SelectorWithWidgetPreference> mSelectorPreferences = new ArrayList<>(); 88 ZenPrioritySendersHelper(Context context, boolean isMessages, ZenModeBackend zenModeBackend, NotificationBackend notificationBackend, SelectorWithWidgetPreference.OnClickListener clickListener)89 public ZenPrioritySendersHelper(Context context, boolean isMessages, 90 ZenModeBackend zenModeBackend, NotificationBackend notificationBackend, 91 SelectorWithWidgetPreference.OnClickListener clickListener) { 92 mContext = context; 93 mIsMessages = isMessages; 94 mZenModeBackend = zenModeBackend; 95 mNotificationBackend = notificationBackend; 96 mSelectorClickListener = clickListener; 97 98 String contactsPackage = context.getString(R.string.config_contacts_package_name); 99 ALL_CONTACTS_INTENT.setPackage(contactsPackage); 100 STARRED_CONTACTS_INTENT.setPackage(contactsPackage); 101 FALLBACK_INTENT.setPackage(contactsPackage); 102 103 mPackageManager = mContext.getPackageManager(); 104 if (!FALLBACK_INTENT.hasCategory(Intent.CATEGORY_APP_CONTACTS)) { 105 FALLBACK_INTENT.addCategory(Intent.CATEGORY_APP_CONTACTS); 106 } 107 } 108 displayPreference(PreferenceCategory preferenceCategory)109 void displayPreference(PreferenceCategory preferenceCategory) { 110 mPreferenceCategory = preferenceCategory; 111 if (mPreferenceCategory.getPreferenceCount() == 0) { 112 makeSelectorPreference(KEY_STARRED, 113 com.android.settings.R.string.zen_mode_from_starred, mIsMessages); 114 makeSelectorPreference(KEY_CONTACTS, 115 com.android.settings.R.string.zen_mode_from_contacts, mIsMessages); 116 if (mIsMessages) { 117 makeSelectorPreference(KEY_IMPORTANT, 118 com.android.settings.R.string.zen_mode_from_important_conversations, true); 119 updateChannelCounts(); 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 updateSummaries(); 126 } 127 } 128 updateState(int currContactsSetting, int currConversationsSetting)129 void updateState(int currContactsSetting, int currConversationsSetting) { 130 for (SelectorWithWidgetPreference pref : mSelectorPreferences) { 131 // for each preference, check whether the current state matches what this state 132 // would look like if the button were checked. 133 final int[] checkedState = keyToSettingEndState(pref.getKey(), true); 134 final int checkedContactsSetting = checkedState[0]; 135 final int checkedConversationsSetting = checkedState[1]; 136 137 boolean match = checkedContactsSetting == currContactsSetting; 138 if (mIsMessages && checkedConversationsSetting != UNKNOWN) { 139 // "UNKNOWN" in checkedContactsSetting means this preference doesn't govern 140 // the priority senders setting, so the full match happens when either 141 // the priority senders setting matches or if it's UNKNOWN so only the conversation 142 // setting needs to match. 143 match = (match || checkedContactsSetting == UNKNOWN) 144 && (checkedConversationsSetting == currConversationsSetting); 145 } 146 147 pref.setChecked(match); 148 } 149 } 150 updateSummaries()151 void updateSummaries() { 152 for (SelectorWithWidgetPreference pref : mSelectorPreferences) { 153 pref.setSummary(getSummary(pref.getKey())); 154 } 155 } 156 157 // Gets the desired end state of the priority senders and conversations for the given key 158 // and whether it is being checked or unchecked. UNKNOWN indicates no change in state. 159 // 160 // Returns an integer array with 2 entries. The first entry is the setting for priority senders 161 // and the second entry is for priority conversation senders; if isMessages is false, then 162 // no changes will ever be prescribed for conversation senders. keyToSettingEndState(String key, boolean checked)163 int[] keyToSettingEndState(String key, boolean checked) { 164 int[] endState = new int[]{ UNKNOWN, UNKNOWN }; 165 if (!checked) { 166 // Unchecking any priority-senders-based state should reset the state to NONE. 167 // "Unchecking" the NONE state doesn't do anything, in practice. 168 switch (key) { 169 case KEY_STARRED: 170 case KEY_CONTACTS: 171 case KEY_ANY: 172 case KEY_NONE: 173 endState[0] = ZenModeBackend.SOURCE_NONE; 174 } 175 176 // For messages, unchecking "priority conversations" and "any" should reset conversation 177 // state to "NONE" as well. 178 if (mIsMessages) { 179 switch (key) { 180 case KEY_IMPORTANT: 181 case KEY_ANY: 182 case KEY_NONE: 183 endState[1] = CONVERSATION_SENDERS_NONE; 184 } 185 } 186 } else { 187 // All below is for the enabling (checked) state. 188 switch (key) { 189 case KEY_STARRED: 190 endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_STARRED; 191 break; 192 case KEY_CONTACTS: 193 endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_CONTACTS; 194 break; 195 case KEY_ANY: 196 endState[0] = NotificationManager.Policy.PRIORITY_SENDERS_ANY; 197 break; 198 case KEY_NONE: 199 endState[0] = ZenModeBackend.SOURCE_NONE; 200 } 201 202 // In the messages case *only*, also handle changing of conversation settings. 203 if (mIsMessages) { 204 switch (key) { 205 case KEY_IMPORTANT: 206 endState[1] = CONVERSATION_SENDERS_IMPORTANT; 207 break; 208 case KEY_ANY: 209 endState[1] = CONVERSATION_SENDERS_ANYONE; 210 break; 211 case KEY_NONE: 212 endState[1] = CONVERSATION_SENDERS_NONE; 213 } 214 } 215 } 216 217 // Error case check: if somehow, after all of that, endState is still {UNKNOWN, UNKNOWN}, 218 // something has gone wrong. 219 if (endState[0] == UNKNOWN && endState[1] == UNKNOWN) { 220 throw new IllegalArgumentException("invalid key " + key); 221 } 222 223 return endState; 224 } 225 226 // Returns the preferences, if any, that should be newly saved for the specified setting and 227 // checked state in an array where index 0 is the new senders setting and 1 the new 228 // conversations setting. A return value of UNKNOWN indicates that nothing should change. 229 // 230 // The returned conversations setting will always be UNKNOWN (not to change) in the calls case. 231 // 232 // Checking and unchecking is mostly an operation of setting or unsetting the relevant 233 // preference, except for some special handling where the conversation setting overlaps: 234 // - setting or unsetting "priority contacts" or "contacts" has no effect on the 235 // priority conversation setting, and vice versa 236 // - if "priority conversations" is selected, and the user checks "anyone", the conversation 237 // setting is also set to any conversations 238 // - if "anyone" is previously selected, and the user clicks "priority conversations", then 239 // the contacts setting is additionally reset to "none". 240 // - if "anyone" is previously selected, and the user clicks one of the contacts values, 241 // then the conversations setting is additionally reset to "none". settingsToSaveOnClick(SelectorWithWidgetPreference preference, int currSendersSetting, int currConvosSetting)242 int[] settingsToSaveOnClick(SelectorWithWidgetPreference preference, 243 int currSendersSetting, int currConvosSetting) { 244 int[] savedSettings = new int[]{ UNKNOWN, UNKNOWN }; 245 246 // If the preference isn't a checkbox, always consider this to be "checking" the setting. 247 // Otherwise, toggle. 248 final int[] endState = keyToSettingEndState(preference.getKey(), 249 preference.isCheckBox() ? !preference.isChecked() : true); 250 final int prioritySendersSetting = endState[0]; 251 final int priorityConvosSetting = endState[1]; 252 253 if (prioritySendersSetting != UNKNOWN && prioritySendersSetting != currSendersSetting) { 254 savedSettings[0] = prioritySendersSetting; 255 } 256 257 // Only handle conversation settings for the messages case. If not messages, there should 258 // never be any change to the conversation senders setting. 259 if (mIsMessages) { 260 if (priorityConvosSetting != UNKNOWN 261 && priorityConvosSetting != currConvosSetting) { 262 savedSettings[1] = priorityConvosSetting; 263 } 264 265 // Special-case handling for the "priority conversations" checkbox: 266 // If a specific selection exists for priority senders (starred, contacts), we leave 267 // it untouched. Otherwise (when the senders is set to "any"), set it to NONE. 268 if (preference.getKey() == KEY_IMPORTANT 269 && currSendersSetting == PRIORITY_SENDERS_ANY) { 270 savedSettings[0] = ZenModeBackend.SOURCE_NONE; 271 } 272 273 // Flip-side special case for clicking either "contacts" option: if a specific selection 274 // exists for priority conversations, leave it untouched; otherwise, set to none. 275 if ((preference.getKey() == KEY_STARRED || preference.getKey() == KEY_CONTACTS) 276 && currConvosSetting == CONVERSATION_SENDERS_ANYONE) { 277 savedSettings[1] = CONVERSATION_SENDERS_NONE; 278 } 279 } 280 281 return savedSettings; 282 } 283 getSummary(String key)284 private String getSummary(String key) { 285 switch (key) { 286 case KEY_STARRED: 287 return mZenModeBackend.getStarredContactsSummary(mContext); 288 case KEY_CONTACTS: 289 return mZenModeBackend.getContactsNumberSummary(mContext); 290 case KEY_IMPORTANT: 291 return getConversationSummary(); 292 case KEY_ANY: 293 return mContext.getResources().getString(mIsMessages 294 ? R.string.zen_mode_all_messages_summary 295 : R.string.zen_mode_all_calls_summary); 296 case KEY_NONE: 297 default: 298 return null; 299 } 300 } 301 getConversationSummary()302 private String getConversationSummary() { 303 final int numConversations = mNumImportantConversations; 304 305 if (numConversations == UNKNOWN) { 306 return null; 307 } else { 308 MessageFormat msgFormat = new MessageFormat( 309 mContext.getString(R.string.zen_mode_conversations_count), 310 Locale.getDefault()); 311 Map<String, Object> args = new HashMap<>(); 312 args.put("count", numConversations); 313 return msgFormat.format(args); 314 } 315 } 316 updateChannelCounts()317 void updateChannelCounts() { 318 // Load conversations 319 ParceledListSlice<ConversationChannelWrapper> impConversations = 320 mNotificationBackend.getConversations(true); 321 int numImportantConversations = 0; 322 if (impConversations != null) { 323 for (ConversationChannelWrapper conversation : impConversations.getList()) { 324 if (!conversation.getNotificationChannel().isDemoted()) { 325 numImportantConversations++; 326 } 327 } 328 } 329 mNumImportantConversations = numImportantConversations; 330 } 331 makeSelectorPreference(String key, int titleId, boolean isCheckbox)332 private SelectorWithWidgetPreference makeSelectorPreference(String key, int titleId, 333 boolean isCheckbox) { 334 final SelectorWithWidgetPreference pref = 335 new SelectorWithWidgetPreference(mPreferenceCategory.getContext(), isCheckbox); 336 pref.setKey(key); 337 pref.setTitle(titleId); 338 pref.setOnClickListener(mSelectorClickListener); 339 340 View.OnClickListener widgetClickListener = getWidgetClickListener(key); 341 if (widgetClickListener != null) { 342 pref.setExtraWidgetOnClickListener(widgetClickListener); 343 } 344 345 mPreferenceCategory.addPreference(pref); 346 mSelectorPreferences.add(pref); 347 return pref; 348 } 349 getWidgetClickListener(String key)350 private View.OnClickListener getWidgetClickListener(String key) { 351 if (!KEY_CONTACTS.equals(key) && !KEY_STARRED.equals(key) && !KEY_IMPORTANT.equals(key)) { 352 return null; 353 } 354 355 if (KEY_STARRED.equals(key) && !isStarredIntentValid()) { 356 return null; 357 } 358 359 if (KEY_CONTACTS.equals(key) && !isContactsIntentValid()) { 360 return null; 361 } 362 363 return new View.OnClickListener() { 364 @Override 365 public void onClick(View v) { 366 if (KEY_STARRED.equals(key) 367 && STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { 368 mContext.startActivity(STARRED_CONTACTS_INTENT); 369 } else if (KEY_CONTACTS.equals(key) 370 && ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null) { 371 mContext.startActivity(ALL_CONTACTS_INTENT); 372 } else if (KEY_IMPORTANT.equals(key)) { 373 new SubSettingLauncher(mContext) 374 .setDestination(ConversationListSettings.class.getName()) 375 .setSourceMetricsCategory(SettingsEnums.DND_CONVERSATIONS) 376 .launch(); 377 } else { 378 mContext.startActivity(FALLBACK_INTENT); 379 } 380 } 381 }; 382 } 383 384 private boolean isStarredIntentValid() { 385 return STARRED_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 386 || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; 387 } 388 389 private boolean isContactsIntentValid() { 390 return ALL_CONTACTS_INTENT.resolveActivity(mPackageManager) != null 391 || FALLBACK_INTENT.resolveActivity(mPackageManager) != null; 392 } 393 } 394