1 /*
2  * Copyright (C) 2020 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.app;
18 
19 import static android.app.NotificationManager.IMPORTANCE_NONE;
20 
21 import android.app.NotificationChannel;
22 import android.app.NotificationChannelGroup;
23 import android.app.settings.SettingsEnums;
24 import android.content.Context;
25 import android.os.AsyncTask;
26 import android.os.Bundle;
27 import android.os.UserHandle;
28 import android.provider.Settings;
29 
30 import androidx.annotation.VisibleForTesting;
31 import androidx.core.text.BidiFormatter;
32 import androidx.lifecycle.LifecycleObserver;
33 import androidx.preference.Preference;
34 import androidx.preference.PreferenceCategory;
35 import androidx.preference.PreferenceScreen;
36 import androidx.preference.TwoStatePreference;
37 
38 import com.android.settings.R;
39 import com.android.settings.applications.AppInfoBase;
40 import com.android.settings.core.PreferenceControllerMixin;
41 import com.android.settings.core.SubSettingLauncher;
42 import com.android.settings.flags.Flags;
43 import com.android.settings.notification.NotificationBackend;
44 import com.android.settingslib.PrimarySwitchPreference;
45 import com.android.settingslib.RestrictedSwitchPreference;
46 
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.HashMap;
50 import java.util.HashSet;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Set;
54 
55 /**
56  * Populates the PreferenceCategory with notification channels associated with the given app.
57  * Users can allow/disallow notification channels from bypassing DND on a single settings
58  * page.
59  */
60 public class AppChannelsBypassingDndPreferenceController extends NotificationPreferenceController
61         implements PreferenceControllerMixin, LifecycleObserver {
62 
63     @VisibleForTesting static final String KEY = "zen_mode_bypassing_app_channels_list";
64     private static final String ARG_FROM_SETTINGS = "fromSettings";
65 
66     private RestrictedSwitchPreference mAllNotificationsToggle;
67     private PreferenceCategory mPreferenceCategory;
68     private List<NotificationChannel> mChannels = new ArrayList<>();
69     private Set<String> mDuplicateChannelNames = new HashSet<>();
70     private Map<NotificationChannel, String> mChannelGroupNames =
71             new HashMap<NotificationChannel, String>();
72 
AppChannelsBypassingDndPreferenceController( Context context, NotificationBackend backend)73     public AppChannelsBypassingDndPreferenceController(
74             Context context,
75             NotificationBackend backend) {
76         super(context, backend);
77     }
78 
79     @Override
displayPreference(PreferenceScreen screen)80     public void displayPreference(PreferenceScreen screen) {
81         mPreferenceCategory = screen.findPreference(KEY);
82 
83         mAllNotificationsToggle = new RestrictedSwitchPreference(mPreferenceCategory.getContext());
84         mAllNotificationsToggle.setTitle(R.string.zen_mode_bypassing_app_channels_toggle_all);
85         mAllNotificationsToggle.setDisabledByAdmin(mAdmin);
86         mAllNotificationsToggle.setEnabled(!mAppRow.banned
87                 && (mAdmin == null || !mAllNotificationsToggle.isDisabledByAdmin()));
88         mAllNotificationsToggle.setOnPreferenceClickListener(
89                 new Preference.OnPreferenceClickListener() {
90                     @Override
91                     public boolean onPreferenceClick(Preference pref) {
92                         TwoStatePreference preference = (TwoStatePreference) pref;
93                         final boolean bypassDnd = preference.isChecked();
94                         for (NotificationChannel channel : mChannels) {
95                             if (showNotification(channel) && isChannelConfigurable(channel)) {
96                                 channel.setBypassDnd(bypassDnd);
97                                 channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
98                                 mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
99                             }
100                         }
101                         // the 0th index is the mAllNotificationsToggle which allows users to
102                         // toggle all notifications from this app to bypass DND
103                         for (int i = 1; i < mPreferenceCategory.getPreferenceCount(); i++) {
104                             PrimarySwitchPreference childPreference =
105                                     (PrimarySwitchPreference) mPreferenceCategory.getPreference(i);
106                             childPreference.setChecked(showNotificationInDnd(mChannels.get(i - 1)));
107                         }
108                         return true;
109                     }
110                 });
111 
112         loadAppChannels();
113         super.displayPreference(screen);
114     }
115 
116     @Override
getPreferenceKey()117     public String getPreferenceKey() {
118         return KEY;
119     }
120 
121     @Override
isAvailable()122     public boolean isAvailable() {
123         return mAppRow != null;
124     }
125 
126     @Override
isIncludedInFilter()127     boolean isIncludedInFilter() {
128         return false;
129     }
130 
131     @Override
updateState(Preference preference)132     public void updateState(Preference preference) {
133         if (mAppRow != null) {
134             loadAppChannels();
135         }
136     }
137 
loadAppChannels()138     private void loadAppChannels() {
139         // Load channel settings
140         new AsyncTask<Void, Void, Void>() {
141             @Override
142             protected Void doInBackground(Void... unused) {
143                 List<NotificationChannel> newChannelList = new ArrayList<>();
144                 List<NotificationChannelGroup> mChannelGroupList = mBackend.getGroups(mAppRow.pkg,
145                         mAppRow.uid).getList();
146                 Set<String> allChannelNames = new HashSet<>();
147                 for (NotificationChannelGroup channelGroup : mChannelGroupList) {
148                     for (NotificationChannel channel : channelGroup.getChannels()) {
149                         if (!isConversation(channel)) {
150                             newChannelList.add(channel);
151                             if (Flags.dedupeDndSettingsChannels()) {
152                                 mChannelGroupNames.put(channel, channelGroup.getName().toString());
153                                 // Check if channel name is unique on this page; if not, save it.
154                                 if (allChannelNames.contains(channel.getName())) {
155                                     mDuplicateChannelNames.add(channel.getName().toString());
156                                 } else {
157                                     allChannelNames.add(channel.getName().toString());
158                                 }
159                             }
160                         }
161                     }
162                 }
163                 Collections.sort(newChannelList, CHANNEL_COMPARATOR);
164                 mChannels = newChannelList;
165                 return null;
166             }
167 
168             @Override
169             protected void onPostExecute(Void unused) {
170                 if (mContext == null) {
171                     return;
172                 }
173                 populateList();
174             }
175         }.execute();
176     }
177 
populateList()178     private void populateList() {
179         if (mPreferenceCategory == null) {
180             return;
181         }
182 
183         mPreferenceCategory.removeAll();
184         mPreferenceCategory.addPreference(mAllNotificationsToggle);
185         for (NotificationChannel channel : mChannels) {
186             PrimarySwitchPreference channelPreference = new PrimarySwitchPreference(mContext);
187             channelPreference.setDisabledByAdmin(mAdmin);
188             channelPreference.setSwitchEnabled(
189                     (mAdmin == null || !channelPreference.isDisabledByAdmin())
190                             && isChannelConfigurable(channel)
191                             && showNotification(channel));
192             channelPreference.setTitle(BidiFormatter.getInstance().unicodeWrap(channel.getName()));
193             if (Flags.dedupeDndSettingsChannels()) {
194                 // If the channel shares its name with another channel, set group name as summary
195                 // to disambiguate in the list.
196                 if (mDuplicateChannelNames.contains(channel.getName().toString())
197                         && mChannelGroupNames.containsKey(channel)
198                         && mChannelGroupNames.get(channel) != null
199                         && !mChannelGroupNames.get(channel).isEmpty()) {
200                     channelPreference.setSummary(BidiFormatter.getInstance().unicodeWrap(
201                             mChannelGroupNames.get(channel)));
202                 }
203             }
204             channelPreference.setChecked(showNotificationInDnd(channel));
205             channelPreference.setOnPreferenceChangeListener(
206                     new Preference.OnPreferenceChangeListener() {
207                         @Override
208                         public boolean onPreferenceChange(Preference pref, Object val) {
209                             boolean switchOn = (Boolean) val;
210                             channel.setBypassDnd(switchOn);
211                             channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
212                             mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
213                             mAllNotificationsToggle.setChecked(areAllChannelsBypassing());
214                             return true;
215                         }
216                     });
217 
218             Bundle channelArgs = new Bundle();
219             channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid);
220             channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg);
221             channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
222             channelArgs.putBoolean(ARG_FROM_SETTINGS, true);
223             channelPreference.setOnPreferenceClickListener(preference -> {
224                 new SubSettingLauncher(mContext)
225                         .setDestination(ChannelNotificationSettings.class.getName())
226                         .setArguments(channelArgs)
227                         .setUserHandle(UserHandle.of(mAppRow.userId))
228                         .setTitleRes(com.android.settings.R.string.notification_channel_title)
229                         .setSourceMetricsCategory(SettingsEnums.DND_APPS_BYPASSING)
230                         .launch();
231                 return true;
232             });
233             mPreferenceCategory.addPreference(channelPreference);
234         }
235         mAllNotificationsToggle.setChecked(areAllChannelsBypassing());
236     }
237 
areAllChannelsBypassing()238     private boolean areAllChannelsBypassing() {
239         if (mAppRow.banned) {
240             return false;
241         }
242         boolean allChannelsBypassing = true;
243         for (NotificationChannel channel : mChannels) {
244             if (showNotification(channel)) {
245                 allChannelsBypassing &= showNotificationInDnd(channel);
246             }
247         }
248         return allChannelsBypassing;
249     }
250 
251     /**
252      * Whether notifications from this channel would show if DND were on.
253      */
showNotificationInDnd(NotificationChannel channel)254     private boolean showNotificationInDnd(NotificationChannel channel) {
255         return channel.canBypassDnd() && showNotification(channel);
256     }
257 
258     /**
259      * Whether notifications from this channel would show if DND weren't on.
260      */
showNotification(NotificationChannel channel)261     private boolean showNotification(NotificationChannel channel) {
262         return !mAppRow.banned && channel.getImportance() != IMPORTANCE_NONE;
263     }
264 
265     /**
266      * Whether this notification channel is representing a conversation.
267      */
isConversation(NotificationChannel channel)268     private boolean isConversation(NotificationChannel channel) {
269         return channel.getConversationId() != null && !channel.isDemoted();
270     }
271 }
272