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