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