1 /*
2  * Copyright (C) 2019 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;
18 
19 import static android.app.NotificationManager.IMPORTANCE_LOW;
20 import static android.app.NotificationManager.IMPORTANCE_NONE;
21 
22 import android.app.NotificationChannel;
23 import android.app.NotificationChannelGroup;
24 import android.app.settings.SettingsEnums;
25 import android.content.Context;
26 import android.graphics.BlendMode;
27 import android.graphics.BlendModeColorFilter;
28 import android.graphics.drawable.Drawable;
29 import android.graphics.drawable.GradientDrawable;
30 import android.graphics.drawable.LayerDrawable;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.provider.Settings;
34 
35 import com.android.settings.R;
36 import com.android.settings.Utils;
37 import com.android.settings.applications.AppInfoBase;
38 import com.android.settings.core.SubSettingLauncher;
39 import com.android.settings.widget.MasterSwitchPreference;
40 import com.android.settingslib.RestrictedSwitchPreference;
41 
42 import java.util.ArrayList;
43 import java.util.Collections;
44 import java.util.Comparator;
45 import java.util.List;
46 
47 import androidx.preference.Preference;
48 import androidx.preference.PreferenceCategory;
49 import androidx.preference.PreferenceGroup;
50 import androidx.preference.SwitchPreference;
51 
52 public class ChannelListPreferenceController extends NotificationPreferenceController {
53 
54     private static final String KEY = "channels";
55     private static String KEY_GENERAL_CATEGORY = "categories";
56     public static final String ARG_FROM_SETTINGS = "fromSettings";
57 
58     private List<NotificationChannelGroup> mChannelGroupList;
59     private PreferenceCategory mPreference;
60 
ChannelListPreferenceController(Context context, NotificationBackend backend)61     public ChannelListPreferenceController(Context context, NotificationBackend backend) {
62         super(context, backend);
63     }
64 
65     @Override
getPreferenceKey()66     public String getPreferenceKey() {
67         return KEY;
68     }
69 
70     @Override
isAvailable()71     public boolean isAvailable() {
72         if (mAppRow == null) {
73             return false;
74         }
75         if (mAppRow.banned) {
76             return false;
77         }
78         if (mChannel != null) {
79             if (mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid)
80                     || NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId())) {
81                 return false;
82             }
83         }
84         return true;
85     }
86 
87     @Override
updateState(Preference preference)88     public void updateState(Preference preference) {
89         mPreference = (PreferenceCategory) preference;
90         // Load channel settings
91         new AsyncTask<Void, Void, Void>() {
92             @Override
93             protected Void doInBackground(Void... unused) {
94                 mChannelGroupList = mBackend.getGroups(mAppRow.pkg, mAppRow.uid).getList();
95                 Collections.sort(mChannelGroupList, mChannelGroupComparator);
96                 return null;
97             }
98 
99             @Override
100             protected void onPostExecute(Void unused) {
101                 if (mContext == null) {
102                     return;
103                 }
104                 populateList();
105             }
106         }.execute();
107     }
108 
populateList()109     private void populateList() {
110         // TODO: if preference has children, compare with newly loaded list
111         mPreference.removeAll();
112 
113         if (mChannelGroupList.isEmpty()) {
114             PreferenceCategory groupCategory = new PreferenceCategory(mContext);
115             groupCategory.setTitle(R.string.notification_channels);
116             groupCategory.setKey(KEY_GENERAL_CATEGORY);
117             mPreference.addPreference(groupCategory);
118 
119             Preference empty = new Preference(mContext);
120             empty.setTitle(R.string.no_channels);
121             empty.setEnabled(false);
122             groupCategory.addPreference(empty);
123         } else {
124             populateGroupList();
125         }
126     }
127 
populateGroupList()128     private void populateGroupList() {
129         for (NotificationChannelGroup group : mChannelGroupList) {
130             PreferenceCategory groupCategory = new PreferenceCategory(mContext);
131             groupCategory.setOrderingAsAdded(true);
132             mPreference.addPreference(groupCategory);
133             if (group.getId() == null) {
134                 if (mChannelGroupList.size() > 1) {
135                     groupCategory.setTitle(R.string.notification_channels_other);
136                 }
137                 groupCategory.setKey(KEY_GENERAL_CATEGORY);
138             } else {
139                 groupCategory.setTitle(group.getName());
140                 groupCategory.setKey(group.getId());
141                 populateGroupToggle(groupCategory, group);
142             }
143             if (!group.isBlocked()) {
144                 final List<NotificationChannel> channels = group.getChannels();
145                 Collections.sort(channels, mChannelComparator);
146                 int N = channels.size();
147                 for (int i = 0; i < N; i++) {
148                     final NotificationChannel channel = channels.get(i);
149                     populateSingleChannelPrefs(groupCategory, channel, group.isBlocked());
150                 }
151             }
152         }
153     }
154 
populateGroupToggle(final PreferenceGroup parent, NotificationChannelGroup group)155     protected void populateGroupToggle(final PreferenceGroup parent,
156             NotificationChannelGroup group) {
157         RestrictedSwitchPreference preference =
158                 new RestrictedSwitchPreference(mContext);
159         preference.setTitle(R.string.notification_switch_label);
160         preference.setEnabled(mAdmin == null
161                 && isChannelGroupBlockable(group));
162         preference.setChecked(!group.isBlocked());
163         preference.setOnPreferenceClickListener(preference1 -> {
164             final boolean allowGroup = ((SwitchPreference) preference1).isChecked();
165             group.setBlocked(!allowGroup);
166             mBackend.updateChannelGroup(mAppRow.pkg, mAppRow.uid, group);
167 
168             onGroupBlockStateChanged(group);
169             return true;
170         });
171 
172         parent.addPreference(preference);
173     }
174 
populateSingleChannelPrefs(PreferenceGroup parent, final NotificationChannel channel, final boolean groupBlocked)175     protected Preference populateSingleChannelPrefs(PreferenceGroup parent,
176             final NotificationChannel channel, final boolean groupBlocked) {
177         MasterSwitchPreference channelPref = new MasterSwitchPreference(mContext);
178         channelPref.setSwitchEnabled(mAdmin == null
179                 && isChannelBlockable(channel)
180                 && isChannelConfigurable(channel)
181                 && !groupBlocked);
182         channelPref.setIcon(null);
183         if (channel.getImportance() > IMPORTANCE_LOW) {
184             channelPref.setIcon(getAlertingIcon());
185         }
186         channelPref.setIconSize(MasterSwitchPreference.ICON_SIZE_SMALL);
187         channelPref.setKey(channel.getId());
188         channelPref.setTitle(channel.getName());
189         channelPref.setSummary(NotificationBackend.getSentSummary(
190                 mContext, mAppRow.sentByChannel.get(channel.getId()), false));
191         channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE);
192         Bundle channelArgs = new Bundle();
193         channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid);
194         channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg);
195         channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
196         channelArgs.putBoolean(ARG_FROM_SETTINGS, true);
197         channelPref.setIntent(new SubSettingLauncher(mContext)
198                 .setDestination(ChannelNotificationSettings.class.getName())
199                 .setArguments(channelArgs)
200                 .setTitleRes(R.string.notification_channel_title)
201                 .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_APP_NOTIFICATION)
202                 .toIntent());
203 
204         channelPref.setOnPreferenceChangeListener(
205                 (preference, o) -> {
206                     boolean value = (Boolean) o;
207                     int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE;
208                     channel.setImportance(importance);
209                     channel.lockFields(
210                             NotificationChannel.USER_LOCKED_IMPORTANCE);
211                     MasterSwitchPreference channelPref1 = (MasterSwitchPreference) preference;
212                     channelPref1.setIcon(null);
213                     if (channel.getImportance() > IMPORTANCE_LOW) {
214                         channelPref1.setIcon(getAlertingIcon());
215                     }
216                     toggleBehaviorIconState(channelPref1.getIcon(),
217                             importance != IMPORTANCE_NONE);
218                     mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
219 
220                     return true;
221                 });
222         if (parent.findPreference(channelPref.getKey()) == null) {
223             parent.addPreference(channelPref);
224         }
225         return channelPref;
226     }
227 
getAlertingIcon()228     private Drawable getAlertingIcon() {
229         Drawable icon = mContext.getDrawable(R.drawable.ic_notifications_alert);
230         icon.setTintList(Utils.getColorAccent(mContext));
231         return icon;
232     }
233 
toggleBehaviorIconState(Drawable icon, boolean enabled)234     private void toggleBehaviorIconState(Drawable icon, boolean enabled) {
235         if (icon == null) return;
236 
237         LayerDrawable layerDrawable = (LayerDrawable) icon;
238         GradientDrawable background =
239                 (GradientDrawable) layerDrawable.findDrawableByLayerId(R.id.back);
240 
241         if (background == null) return;
242 
243         if (enabled) {
244             background.clearColorFilter();
245         } else {
246             background.setColorFilter(new BlendModeColorFilter(
247                     mContext.getColor(R.color.material_grey_300),
248                     BlendMode.SRC_IN));
249         }
250     }
251 
onGroupBlockStateChanged(NotificationChannelGroup group)252     protected void onGroupBlockStateChanged(NotificationChannelGroup group) {
253         if (group == null) {
254             return;
255         }
256         PreferenceGroup groupGroup = mPreference.findPreference(group.getId());
257 
258         if (groupGroup != null) {
259             if (group.isBlocked()) {
260                 List<Preference> toRemove = new ArrayList<>();
261                 int childCount = groupGroup.getPreferenceCount();
262                 for (int i = 0; i < childCount; i++) {
263                     Preference pref = groupGroup.getPreference(i);
264                     if (pref instanceof MasterSwitchPreference) {
265                         toRemove.add(pref);
266                     }
267                 }
268                 for (Preference pref : toRemove) {
269                     groupGroup.removePreference(pref);
270                 }
271             } else {
272                 final List<NotificationChannel> channels = group.getChannels();
273                 Collections.sort(channels, mChannelComparator);
274                 int N = channels.size();
275                 for (int i = 0; i < N; i++) {
276                     final NotificationChannel channel = channels.get(i);
277                     populateSingleChannelPrefs(groupGroup, channel, group.isBlocked());
278                 }
279             }
280         }
281     }
282 
283     private Comparator<NotificationChannelGroup> mChannelGroupComparator =
284             new Comparator<NotificationChannelGroup>() {
285 
286                 @Override
287                 public int compare(NotificationChannelGroup left, NotificationChannelGroup right) {
288                     // Non-grouped channels (in placeholder group with a null id) come last
289                     if (left.getId() == null && right.getId() != null) {
290                         return 1;
291                     } else if (right.getId() == null && left.getId() != null) {
292                         return -1;
293                     }
294                     return left.getId().compareTo(right.getId());
295                 }
296             };
297 
298     protected Comparator<NotificationChannel> mChannelComparator =
299             (left, right) -> {
300                 if (left.isDeleted() != right.isDeleted()) {
301                     return Boolean.compare(left.isDeleted(), right.isDeleted());
302                 } else if (left.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
303                     // Uncategorized/miscellaneous legacy channel goes last
304                     return 1;
305                 } else if (right.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
306                     return -1;
307                 }
308 
309                 return left.getId().compareTo(right.getId());
310             };
311 }
312