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