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