• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.SwitchPreference;
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.notification.NotificationBackend;
43 import com.android.settingslib.PrimarySwitchPreference;
44 import com.android.settingslib.RestrictedSwitchPreference;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.List;
49 
50 /**
51  * Populates the PreferenceCategory with notification channels associated with the given app.
52  * Users can allow/disallow notification channels from bypassing DND on a single settings
53  * page.
54  */
55 public class AppChannelsBypassingDndPreferenceController extends NotificationPreferenceController
56         implements PreferenceControllerMixin, LifecycleObserver {
57 
58     @VisibleForTesting static final String KEY = "zen_mode_bypassing_app_channels_list";
59     private static final String ARG_FROM_SETTINGS = "fromSettings";
60 
61     private RestrictedSwitchPreference mAllNotificationsToggle;
62     private PreferenceCategory mPreferenceCategory;
63     private List<NotificationChannel> mChannels = new ArrayList<>();
64 
AppChannelsBypassingDndPreferenceController( Context context, NotificationBackend backend)65     public AppChannelsBypassingDndPreferenceController(
66             Context context,
67             NotificationBackend backend) {
68         super(context, backend);
69     }
70 
71     @Override
displayPreference(PreferenceScreen screen)72     public void displayPreference(PreferenceScreen screen) {
73         mPreferenceCategory = screen.findPreference(KEY);
74 
75         mAllNotificationsToggle = new RestrictedSwitchPreference(mPreferenceCategory.getContext());
76         mAllNotificationsToggle.setTitle(R.string.zen_mode_bypassing_app_channels_toggle_all);
77         mAllNotificationsToggle.setDisabledByAdmin(mAdmin);
78         mAllNotificationsToggle.setEnabled(!mAppRow.banned
79                 && (mAdmin == null || !mAllNotificationsToggle.isDisabledByAdmin()));
80         mAllNotificationsToggle.setOnPreferenceClickListener(
81                 new Preference.OnPreferenceClickListener() {
82                     @Override
83                     public boolean onPreferenceClick(Preference pref) {
84                         SwitchPreference preference = (SwitchPreference) pref;
85                         final boolean bypassDnd = preference.isChecked();
86                         for (NotificationChannel channel : mChannels) {
87                             if (showNotification(channel) && isChannelConfigurable(channel)) {
88                                 channel.setBypassDnd(bypassDnd);
89                                 channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
90                                 mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
91                             }
92                         }
93                         // the 0th index is the mAllNotificationsToggle which allows users to
94                         // toggle all notifications from this app to bypass DND
95                         for (int i = 1; i < mPreferenceCategory.getPreferenceCount(); i++) {
96                             PrimarySwitchPreference childPreference =
97                                     (PrimarySwitchPreference) mPreferenceCategory.getPreference(i);
98                             childPreference.setChecked(showNotificationInDnd(mChannels.get(i - 1)));
99                         }
100                         return true;
101                     }
102                 });
103 
104         loadAppChannels();
105         super.displayPreference(screen);
106     }
107 
108     @Override
getPreferenceKey()109     public String getPreferenceKey() {
110         return KEY;
111     }
112 
113     @Override
isAvailable()114     public boolean isAvailable() {
115         return mAppRow != null;
116     }
117 
118     @Override
isIncludedInFilter()119     boolean isIncludedInFilter() {
120         return false;
121     }
122 
123     @Override
updateState(Preference preference)124     public void updateState(Preference preference) {
125         if (mAppRow != null) {
126             loadAppChannels();
127         }
128     }
129 
loadAppChannels()130     private void loadAppChannels() {
131         // Load channel settings
132         new AsyncTask<Void, Void, Void>() {
133             @Override
134             protected Void doInBackground(Void... unused) {
135                 List<NotificationChannel> newChannelList = new ArrayList<>();
136                 List<NotificationChannelGroup> mChannelGroupList = mBackend.getGroups(mAppRow.pkg,
137                         mAppRow.uid).getList();
138                 for (NotificationChannelGroup channelGroup : mChannelGroupList) {
139                     for (NotificationChannel channel : channelGroup.getChannels()) {
140                         if (!isConversation(channel)) {
141                             newChannelList.add(channel);
142                         }
143                     }
144                 }
145                 Collections.sort(newChannelList, CHANNEL_COMPARATOR);
146                 mChannels = newChannelList;
147                 return null;
148             }
149 
150             @Override
151             protected void onPostExecute(Void unused) {
152                 if (mContext == null) {
153                     return;
154                 }
155                 populateList();
156             }
157         }.execute();
158     }
159 
populateList()160     private void populateList() {
161         if (mPreferenceCategory == null) {
162             return;
163         }
164 
165         mPreferenceCategory.removeAll();
166         mPreferenceCategory.addPreference(mAllNotificationsToggle);
167         for (NotificationChannel channel : mChannels) {
168             PrimarySwitchPreference channelPreference = new PrimarySwitchPreference(mContext);
169             channelPreference.setDisabledByAdmin(mAdmin);
170             channelPreference.setSwitchEnabled(
171                     (mAdmin == null || !channelPreference.isDisabledByAdmin())
172                             && isChannelConfigurable(channel)
173                             && showNotification(channel));
174             channelPreference.setTitle(BidiFormatter.getInstance().unicodeWrap(channel.getName()));
175             channelPreference.setChecked(showNotificationInDnd(channel));
176             channelPreference.setOnPreferenceChangeListener(
177                     new Preference.OnPreferenceChangeListener() {
178                         @Override
179                         public boolean onPreferenceChange(Preference pref, Object val) {
180                             boolean switchOn = (Boolean) val;
181                             channel.setBypassDnd(switchOn);
182                             channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
183                             mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
184                             mAllNotificationsToggle.setChecked(areAllChannelsBypassing());
185                             return true;
186                         }
187                     });
188 
189             Bundle channelArgs = new Bundle();
190             channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid);
191             channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg);
192             channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
193             channelArgs.putBoolean(ARG_FROM_SETTINGS, true);
194             channelPreference.setOnPreferenceClickListener(preference -> {
195                 new SubSettingLauncher(mContext)
196                         .setDestination(ChannelNotificationSettings.class.getName())
197                         .setArguments(channelArgs)
198                         .setUserHandle(UserHandle.of(mAppRow.userId))
199                         .setTitleRes(com.android.settings.R.string.notification_channel_title)
200                         .setSourceMetricsCategory(SettingsEnums.DND_APPS_BYPASSING)
201                         .launch();
202                 return true;
203             });
204             mPreferenceCategory.addPreference(channelPreference);
205         }
206         mAllNotificationsToggle.setChecked(areAllChannelsBypassing());
207     }
208 
areAllChannelsBypassing()209     private boolean areAllChannelsBypassing() {
210         if (mAppRow.banned) {
211             return false;
212         }
213         boolean allChannelsBypassing = true;
214         for (NotificationChannel channel : mChannels) {
215             if (showNotification(channel)) {
216                 allChannelsBypassing &= showNotificationInDnd(channel);
217             }
218         }
219         return allChannelsBypassing;
220     }
221 
222     /**
223      * Whether notifications from this channel would show if DND were on.
224      */
showNotificationInDnd(NotificationChannel channel)225     private boolean showNotificationInDnd(NotificationChannel channel) {
226         return channel.canBypassDnd() && showNotification(channel);
227     }
228 
229     /**
230      * Whether notifications from this channel would show if DND weren't on.
231      */
showNotification(NotificationChannel channel)232     private boolean showNotification(NotificationChannel channel) {
233         return !mAppRow.banned && channel.getImportance() != IMPORTANCE_NONE;
234     }
235 
236     /**
237      * Whether this notification channel is representing a conversation.
238      */
isConversation(NotificationChannel channel)239     private boolean isConversation(NotificationChannel channel) {
240         return channel.getConversationId() != null && !channel.isDemoted();
241     }
242 }
243