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.app;
18 
19 import static android.app.NotificationManager.IMPORTANCE_LOW;
20 import static android.app.NotificationManager.IMPORTANCE_NONE;
21 import static com.android.server.notification.Flags.notificationHideUnusedChannels;
22 
23 import android.app.NotificationChannel;
24 import android.app.NotificationChannelGroup;
25 import android.app.settings.SettingsEnums;
26 import android.content.Context;
27 import android.graphics.drawable.Drawable;
28 import android.os.AsyncTask;
29 import android.os.Bundle;
30 import android.provider.Settings;
31 import android.text.TextUtils;
32 
33 import androidx.annotation.NonNull;
34 import androidx.annotation.Nullable;
35 import androidx.preference.Preference;
36 import androidx.preference.PreferenceCategory;
37 import androidx.preference.PreferenceGroup;
38 import androidx.preference.TwoStatePreference;
39 
40 import com.android.settings.R;
41 import com.android.settings.Utils;
42 import com.android.settings.applications.AppInfoBase;
43 import com.android.settings.core.SubSettingLauncher;
44 import com.android.settings.notification.NotificationBackend;
45 import com.android.settingslib.PrimarySwitchPreference;
46 import com.android.settingslib.RestrictedSwitchPreference;
47 
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.List;
51 
52 public class ChannelListPreferenceController extends NotificationPreferenceController {
53 
54     private static final String KEY = "channels";
55     private static final String KEY_GENERAL_CATEGORY = "categories";
56     private static final String KEY_ZERO_CATEGORIES = "zeroCategories";
57     public static final String ARG_FROM_SETTINGS = "fromSettings";
58 
59     private List<NotificationChannelGroup> mChannelGroupList;
60     private PreferenceCategory mPreference;
61 
62     private boolean mShowAll;
63 
ChannelListPreferenceController(Context context, NotificationBackend backend)64     public ChannelListPreferenceController(Context context, NotificationBackend backend) {
65         super(context, backend);
66     }
67 
68     @Override
getPreferenceKey()69     public String getPreferenceKey() {
70         return KEY;
71     }
72 
73     @Override
isAvailable()74     public boolean isAvailable() {
75         if (mAppRow == null) {
76             return false;
77         }
78         if (mAppRow.banned) {
79             return false;
80         }
81         if (mChannel != null) {
82             if (mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid)
83                     || NotificationChannel.DEFAULT_CHANNEL_ID.equals(mChannel.getId())) {
84                 return false;
85             }
86         }
87         return true;
88     }
89 
90     @Override
isIncludedInFilter()91     boolean isIncludedInFilter() {
92         return false;
93     }
94 
95     @Override
updateState(Preference preference)96     public void updateState(Preference preference) {
97         mPreference = (PreferenceCategory) preference;
98         // Load channel settings
99         new AsyncTask<Void, Void, Void>() {
100             @Override
101             protected Void doInBackground(Void... unused) {
102                 if (notificationHideUnusedChannels()) {
103                     if (mShowAll) {
104                         mChannelGroupList = mBackend.getGroups(mAppRow.pkg, mAppRow.uid).getList();
105                     } else {
106                         mChannelGroupList = mBackend.getGroupsWithRecentBlockedFilter(mAppRow.pkg,
107                                 mAppRow.uid).getList();
108                     }
109                 } else {
110                     mChannelGroupList = mBackend.getGroups(mAppRow.pkg, mAppRow.uid).getList();
111                 }
112                 Collections.sort(mChannelGroupList, CHANNEL_GROUP_COMPARATOR);
113                 return null;
114             }
115 
116             @Override
117             protected void onPostExecute(Void unused) {
118                 if (mContext == null) {
119                     return;
120                 }
121                 updateFullList(mPreference, mChannelGroupList);
122             }
123         }.execute();
124     }
125 
setShowAll(boolean showAll)126     protected void setShowAll(boolean showAll) {
127         mShowAll = showAll;
128     }
129 
130     /**
131      * Update the preferences group to match the
132      * @param groupPrefsList
133      * @param channelGroups
134      */
updateFullList(@onNull PreferenceCategory groupPrefsList, @NonNull List<NotificationChannelGroup> channelGroups)135     void updateFullList(@NonNull PreferenceCategory groupPrefsList,
136                 @NonNull List<NotificationChannelGroup> channelGroups) {
137         if (channelGroups.isEmpty()) {
138             if (groupPrefsList.getPreferenceCount() == 1
139                     && KEY_ZERO_CATEGORIES.equals(groupPrefsList.getPreference(0).getKey())) {
140                 // Ensure the titles are correct for the current language, but otherwise leave alone
141                 PreferenceGroup groupCategory = (PreferenceGroup) groupPrefsList.getPreference(0);
142                 groupCategory.setTitle(R.string.notification_channels);
143                 groupCategory.getPreference(0).setTitle(R.string.no_channels);
144             } else {
145                 // Clear any contents and create the 'zero-categories' group.
146                 groupPrefsList.removeAll();
147 
148                 PreferenceCategory groupCategory = new PreferenceCategory(mContext);
149                 groupCategory.setTitle(R.string.notification_channels);
150                 groupCategory.setKey(KEY_ZERO_CATEGORIES);
151                 groupPrefsList.addPreference(groupCategory);
152 
153                 Preference empty = new Preference(mContext);
154                 empty.setTitle(R.string.no_channels);
155                 empty.setEnabled(false);
156                 groupCategory.addPreference(empty);
157             }
158         } else {
159             updateGroupList(groupPrefsList, channelGroups);
160         }
161     }
162 
163     /**
164      * Looks for the category for the given group's key at the expected index, if that doesn't
165      * match, it checks all groups, and if it can't find that group anywhere, it creates it.
166      */
167     @NonNull
findOrCreateGroupCategoryForKey( @onNull PreferenceCategory groupPrefsList, @Nullable String key, int expectedIndex)168     private PreferenceCategory findOrCreateGroupCategoryForKey(
169             @NonNull PreferenceCategory groupPrefsList, @Nullable String key, int expectedIndex) {
170         if (key == null) {
171             key = KEY_GENERAL_CATEGORY;
172         }
173         int preferenceCount = groupPrefsList.getPreferenceCount();
174         if (expectedIndex < preferenceCount) {
175             Preference preference = groupPrefsList.getPreference(expectedIndex);
176             if (key.equals(preference.getKey())) {
177                 return (PreferenceCategory) preference;
178             }
179         }
180         for (int i = 0; i < preferenceCount; i++) {
181             Preference preference = groupPrefsList.getPreference(i);
182             if (key.equals(preference.getKey())) {
183                 preference.setOrder(expectedIndex);
184                 return (PreferenceCategory) preference;
185             }
186         }
187         PreferenceCategory groupCategory = new PreferenceCategory(mContext);
188         groupCategory.setOrder(expectedIndex);
189         groupCategory.setKey(key);
190         groupPrefsList.addPreference(groupCategory);
191         return groupCategory;
192     }
193 
updateGroupList(@onNull PreferenceCategory groupPrefsList, @NonNull List<NotificationChannelGroup> channelGroups)194     private void updateGroupList(@NonNull PreferenceCategory groupPrefsList,
195             @NonNull List<NotificationChannelGroup> channelGroups) {
196         // Update the list, but optimize for the most common case where the list hasn't changed.
197         int numFinalGroups = channelGroups.size();
198         int initialPrefCount = groupPrefsList.getPreferenceCount();
199         List<PreferenceCategory> finalOrderedGroups = new ArrayList<>(numFinalGroups);
200         for (int i = 0; i < numFinalGroups; i++) {
201             NotificationChannelGroup group = channelGroups.get(i);
202             PreferenceCategory groupCategory =
203                     findOrCreateGroupCategoryForKey(groupPrefsList, group.getId(), i);
204             finalOrderedGroups.add(groupCategory);
205             updateGroupPreferences(group, groupCategory);
206         }
207         int postAddPrefCount = groupPrefsList.getPreferenceCount();
208         // If any groups were inserted (into a non-empty list) or need to be removed, we need to
209         // remove all groups and re-add them all.
210         // This is required to ensure proper ordering of inserted groups, and it simplifies logic
211         // at the cost of computation in the rare case that the list is changing.
212         boolean hasInsertions = initialPrefCount != 0 && initialPrefCount != numFinalGroups;
213         boolean requiresRemoval = postAddPrefCount != numFinalGroups;
214         if (hasInsertions || requiresRemoval) {
215             groupPrefsList.removeAll();
216             for (PreferenceCategory group : finalOrderedGroups) {
217                 groupPrefsList.addPreference(group);
218             }
219         }
220     }
221 
222     /**
223      * Looks for the channel preference for the given channel's key at the expected index, if that
224      * doesn't match, it checks all rows, and if it can't find that channel anywhere, it creates
225      * the preference.
226      */
227     @NonNull
findOrCreateChannelPrefForKey( @onNull PreferenceGroup groupPrefGroup, @NonNull String key, int expectedIndex)228     private PrimarySwitchPreference findOrCreateChannelPrefForKey(
229             @NonNull PreferenceGroup groupPrefGroup, @NonNull String key, int expectedIndex) {
230         int preferenceCount = groupPrefGroup.getPreferenceCount();
231         if (expectedIndex < preferenceCount) {
232             Preference preference = groupPrefGroup.getPreference(expectedIndex);
233             if (key.equals(preference.getKey())) {
234                 return (PrimarySwitchPreference) preference;
235             }
236         }
237         for (int i = 0; i < preferenceCount; i++) {
238             Preference preference = groupPrefGroup.getPreference(i);
239             if (key.equals(preference.getKey())) {
240                 preference.setOrder(expectedIndex);
241                 return (PrimarySwitchPreference) preference;
242             }
243         }
244         PrimarySwitchPreference channelPref = new PrimarySwitchPreference(mContext);
245         channelPref.setOrder(expectedIndex);
246         channelPref.setKey(key);
247         groupPrefGroup.addPreference(channelPref);
248         return channelPref;
249     }
250 
updateGroupPreferences(@onNull NotificationChannelGroup group, @NonNull PreferenceGroup groupPrefGroup)251     private void updateGroupPreferences(@NonNull NotificationChannelGroup group,
252             @NonNull PreferenceGroup groupPrefGroup) {
253         int initialPrefCount = groupPrefGroup.getPreferenceCount();
254         List<Preference> finalOrderedPrefs = new ArrayList<>();
255         Preference appDefinedGroupToggle;
256         if (group.getId() == null) {
257             // For the 'null' group, set the "Other" title.
258             groupPrefGroup.setTitle(R.string.notification_channels_other);
259             appDefinedGroupToggle = null;
260         } else {
261             // For an app-defined group, set their name and create a row to toggle 'isBlocked'.
262             groupPrefGroup.setTitle(group.getName());
263             appDefinedGroupToggle = addOrUpdateGroupToggle(groupPrefGroup, group);
264             finalOrderedPrefs.add(appDefinedGroupToggle);
265         }
266         // Here "empty" means having no channel rows; the group toggle is ignored for this purpose.
267         boolean initiallyEmpty = groupPrefGroup.getPreferenceCount() == finalOrderedPrefs.size();
268 
269         // For each channel, add or update the preference object.
270         final List<NotificationChannel> channels =
271                 group.isBlocked() ? Collections.emptyList() : group.getChannels();
272         Collections.sort(channels, CHANNEL_COMPARATOR);
273         for (NotificationChannel channel : channels) {
274             if (!TextUtils.isEmpty(channel.getConversationId()) && !channel.isDemoted()) {
275                 // conversations get their own section
276                 continue;
277             }
278             // Get or create the row, and populate its current state.
279             PrimarySwitchPreference channelPref = findOrCreateChannelPrefForKey(groupPrefGroup,
280                     channel.getId(), /* expectedIndex */ finalOrderedPrefs.size());
281             updateSingleChannelPrefs(channelPref, channel, group.isBlocked());
282             finalOrderedPrefs.add(channelPref);
283         }
284         int postAddPrefCount = groupPrefGroup.getPreferenceCount();
285 
286         // If any channels were inserted (into a non-empty list) or need to be removed, we need to
287         // remove all preferences and re-add them all.
288         // This is required to ensure proper ordering of inserted channels, and it simplifies logic
289         // at the cost of computation in the rare case that the list is changing.
290         // As an optimization, keep the app-defined-group toggle. That way it doesn't "flicker"
291         // (due to remove+add) when toggling the group.
292         int numFinalGroups = finalOrderedPrefs.size();
293         boolean hasInsertions = !initiallyEmpty && initialPrefCount != numFinalGroups;
294         boolean requiresRemoval = postAddPrefCount != numFinalGroups;
295         boolean keepGroupToggle =
296                 appDefinedGroupToggle != null && groupPrefGroup.getPreferenceCount() > 0
297                         && groupPrefGroup.getPreference(0) == appDefinedGroupToggle
298                         && finalOrderedPrefs.get(0) == appDefinedGroupToggle;
299         if (hasInsertions || requiresRemoval) {
300             if (keepGroupToggle) {
301                 while (groupPrefGroup.getPreferenceCount() > 1) {
302                     groupPrefGroup.removePreference(groupPrefGroup.getPreference(1));
303                 }
304             } else {
305                 groupPrefGroup.removeAll();
306             }
307             for (int i = (keepGroupToggle ? 1 : 0); i < finalOrderedPrefs.size(); i++) {
308                 groupPrefGroup.addPreference(finalOrderedPrefs.get(i));
309             }
310         }
311     }
312 
313     /** Add or find and update the toggle for disabling the entire notification channel group. */
addOrUpdateGroupToggle(@onNull final PreferenceGroup parent, @NonNull final NotificationChannelGroup group)314     private Preference addOrUpdateGroupToggle(@NonNull final PreferenceGroup parent,
315             @NonNull final NotificationChannelGroup group) {
316         boolean shouldAdd = false;
317         final RestrictedSwitchPreference preference;
318         if (parent.getPreferenceCount() > 0
319                 && parent.getPreference(0) instanceof RestrictedSwitchPreference) {
320             preference = (RestrictedSwitchPreference) parent.getPreference(0);
321         } else {
322             shouldAdd = true;
323             preference = new RestrictedSwitchPreference(mContext);
324         }
325         preference.setOrder(-1);
326         preference.setTitle(mContext.getString(
327                 R.string.notification_switch_label, group.getName()));
328         preference.setEnabled(mAdmin == null
329                 && isChannelGroupBlockable(group));
330         preference.setChecked(!group.isBlocked());
331         preference.setOnPreferenceClickListener(preference1 -> {
332             final boolean allowGroup = ((TwoStatePreference) preference1).isChecked();
333             group.setBlocked(!allowGroup);
334             mBackend.updateChannelGroup(mAppRow.pkg, mAppRow.uid, group);
335 
336             onGroupBlockStateChanged(group);
337             return true;
338         });
339         if (shouldAdd) {
340             parent.addPreference(preference);
341         }
342         return preference;
343     }
344 
345     /** Update the properties of the channel preference with the values from the channel object. */
updateSingleChannelPrefs(@onNull final PrimarySwitchPreference channelPref, @NonNull final NotificationChannel channel, final boolean groupBlocked)346     private void updateSingleChannelPrefs(@NonNull final PrimarySwitchPreference channelPref,
347             @NonNull final NotificationChannel channel,
348             final boolean groupBlocked) {
349         channelPref.setSwitchEnabled(mAdmin == null
350                 && isChannelBlockable(channel)
351                 && isChannelConfigurable(channel)
352                 && !groupBlocked);
353         if (channel.getImportance() > IMPORTANCE_LOW) {
354             channelPref.setIcon(getAlertingIcon());
355         } else {
356             channelPref.setIcon(mContext.getDrawable(R.drawable.empty_icon));
357         }
358         channelPref.setIconSize(PrimarySwitchPreference.ICON_SIZE_SMALL);
359         channelPref.setTitle(channel.getName());
360         channelPref.setSummary(NotificationBackend.getSentSummary(
361                 mContext, mAppRow.sentByChannel.get(channel.getId()), false));
362         channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE);
363         Bundle channelArgs = new Bundle();
364         channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mAppRow.uid);
365         channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mAppRow.pkg);
366         channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId());
367         channelArgs.putBoolean(ARG_FROM_SETTINGS, true);
368         channelPref.setIntent(new SubSettingLauncher(mContext)
369                 .setDestination(ChannelNotificationSettings.class.getName())
370                 .setArguments(channelArgs)
371                 .setTitleRes(R.string.notification_channel_title)
372                 .setSourceMetricsCategory(SettingsEnums.NOTIFICATION_APP_NOTIFICATION)
373                 .toIntent());
374 
375         channelPref.setOnPreferenceChangeListener(
376                 (preference, o) -> {
377                     boolean value = (Boolean) o;
378                     int importance = value
379                             ? Math.max(channel.getOriginalImportance(), IMPORTANCE_LOW)
380                             : IMPORTANCE_NONE;
381                     channel.setImportance(importance);
382                     channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
383                     PrimarySwitchPreference channelPref1 = (PrimarySwitchPreference) preference;
384                     channelPref1.setIcon(R.drawable.empty_icon);
385                     if (channel.getImportance() > IMPORTANCE_LOW) {
386                         channelPref1.setIcon(getAlertingIcon());
387                     }
388                     mBackend.updateChannel(mAppRow.pkg, mAppRow.uid, channel);
389 
390                     return true;
391                 });
392     }
393 
getAlertingIcon()394     private Drawable getAlertingIcon() {
395         Drawable icon = mContext.getDrawable(R.drawable.ic_notifications_alert);
396         icon.setTintList(Utils.getColorAccent(mContext));
397         return icon;
398     }
399 
onGroupBlockStateChanged(NotificationChannelGroup group)400     protected void onGroupBlockStateChanged(NotificationChannelGroup group) {
401         if (group == null) {
402             return;
403         }
404         PreferenceGroup groupPrefGroup = mPreference.findPreference(group.getId());
405         if (groupPrefGroup != null) {
406             updateGroupPreferences(group, groupPrefGroup);
407         }
408     }
409 }
410