• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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