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