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