1 /* 2 * Copyright (C) 2014 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 android.app.Activity; 20 import android.app.NotificationChannel; 21 import android.app.NotificationChannelGroup; 22 import android.app.NotificationManager; 23 import android.content.Intent; 24 import android.net.Uri; 25 import android.os.AsyncTask; 26 import android.os.Bundle; 27 import android.provider.Settings; 28 import android.support.v7.preference.Preference; 29 import android.support.v7.preference.PreferenceCategory; 30 import android.text.TextUtils; 31 import android.util.ArrayMap; 32 import android.util.Log; 33 import android.view.LayoutInflater; 34 import android.view.View; 35 import android.widget.Switch; 36 37 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 38 import com.android.settings.R; 39 import com.android.settings.Utils; 40 import com.android.settings.applications.AppInfoBase; 41 import com.android.settings.applications.LayoutPreference; 42 import com.android.settings.notification.NotificationBackend.AppRow; 43 import com.android.settings.widget.EntityHeaderController; 44 import com.android.settings.widget.MasterSwitchPreference; 45 import com.android.settings.widget.SwitchBar; 46 import com.android.settingslib.RestrictedSwitchPreference; 47 import com.android.settingslib.widget.FooterPreference; 48 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.Comparator; 52 import java.util.List; 53 54 import static android.app.NotificationManager.IMPORTANCE_LOW; 55 import static android.app.NotificationManager.IMPORTANCE_NONE; 56 import static android.app.NotificationManager.IMPORTANCE_UNSPECIFIED; 57 58 /** These settings are per app, so should not be returned in global search results. */ 59 public class AppNotificationSettings extends NotificationSettingsBase { 60 private static final String TAG = "AppNotificationSettings"; 61 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 62 63 private static String KEY_GENERAL_CATEGORY = "categories"; 64 private static String KEY_DELETED = "deleted"; 65 66 private List<NotificationChannelGroup> mChannelGroupList; 67 private List<PreferenceCategory> mChannelGroups = new ArrayList(); 68 private FooterPreference mDeletedChannels; 69 70 @Override 71 public int getMetricsCategory() { 72 return MetricsEvent.NOTIFICATION_APP_NOTIFICATION; 73 } 74 75 @Override 76 public void onResume() { 77 super.onResume(); 78 79 if (mUid < 0 || TextUtils.isEmpty(mPkg) || mPkgInfo == null) { 80 Log.w(TAG, "Missing package or uid or packageinfo"); 81 finish(); 82 return; 83 } 84 85 if (getPreferenceScreen() != null) { 86 getPreferenceScreen().removeAll(); 87 mChannelGroups.clear(); 88 mDeletedChannels = null; 89 mShowLegacyChannelConfig = false; 90 } 91 92 addPreferencesFromResource(R.xml.notification_settings); 93 getPreferenceScreen().setOrderingAsAdded(true); 94 setupBlock(); 95 addHeaderPref(); 96 97 mShowLegacyChannelConfig = mBackend.onlyHasDefaultChannel(mAppRow.pkg, mAppRow.uid); 98 if (mShowLegacyChannelConfig) { 99 mChannel = mBackend.getChannel( 100 mAppRow.pkg, mAppRow.uid, NotificationChannel.DEFAULT_CHANNEL_ID); 101 populateDefaultChannelPrefs(); 102 } else { 103 addPreferencesFromResource(R.xml.upgraded_app_notification_settings); 104 setupBadge(); 105 // Load channel settings 106 new AsyncTask<Void, Void, Void>() { 107 @Override 108 protected Void doInBackground(Void... unused) { 109 mChannelGroupList = mBackend.getChannelGroups(mPkg, mUid).getList(); 110 Collections.sort(mChannelGroupList, mChannelGroupComparator); 111 return null; 112 } 113 114 @Override 115 protected void onPostExecute(Void unused) { 116 if (getHost() == null) { 117 return; 118 } 119 populateChannelList(); 120 addAppLinkPref(); 121 } 122 }.execute(); 123 } 124 125 updateDependents(mAppRow.banned); 126 } 127 128 private void addHeaderPref() { 129 ArrayMap<String, AppRow> rows = new ArrayMap<>(); 130 rows.put(mAppRow.pkg, mAppRow); 131 collectConfigActivities(rows); 132 final Activity activity = getActivity(); 133 final Preference pref = EntityHeaderController 134 .newInstance(activity, this /* fragment */, null /* header */) 135 .setRecyclerView(getListView(), getLifecycle()) 136 .setIcon(mAppRow.icon) 137 .setLabel(mAppRow.label) 138 .setPackageName(mAppRow.pkg) 139 .setUid(mAppRow.uid) 140 .setHasAppInfoLink(true) 141 .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE, 142 EntityHeaderController.ActionType.ACTION_NOTIF_PREFERENCE) 143 .done(activity, getPrefContext()); 144 pref.setKey(KEY_HEADER); 145 getPreferenceScreen().addPreference(pref); 146 } 147 148 private void populateChannelList() { 149 if (!mChannelGroups.isEmpty()) { 150 // If there's anything in mChannelGroups, we've called populateChannelList twice. 151 // Clear out existing channels and log. 152 Log.w(TAG, "Notification channel group posted twice to settings - old size " + 153 mChannelGroups.size() + ", new size " + mChannelGroupList.size()); 154 for (Preference p : mChannelGroups) { 155 getPreferenceScreen().removePreference(p); 156 } 157 } 158 if (mChannelGroupList.isEmpty()) { 159 PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext()); 160 groupCategory.setTitle(R.string.notification_channels); 161 groupCategory.setKey(KEY_GENERAL_CATEGORY); 162 getPreferenceScreen().addPreference(groupCategory); 163 mChannelGroups.add(groupCategory); 164 165 Preference empty = new Preference(getPrefContext()); 166 empty.setTitle(R.string.no_channels); 167 empty.setEnabled(false); 168 groupCategory.addPreference(empty); 169 } else { 170 for (NotificationChannelGroup group : mChannelGroupList) { 171 PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext()); 172 if (group.getId() == null) { 173 groupCategory.setTitle(mChannelGroupList.size() > 1 174 ? R.string.notification_channels_other 175 : R.string.notification_channels); 176 groupCategory.setKey(KEY_GENERAL_CATEGORY); 177 } else { 178 groupCategory.setTitle(group.getName()); 179 groupCategory.setKey(group.getId()); 180 } 181 groupCategory.setOrderingAsAdded(true); 182 getPreferenceScreen().addPreference(groupCategory); 183 mChannelGroups.add(groupCategory); 184 185 final List<NotificationChannel> channels = group.getChannels(); 186 Collections.sort(channels, mChannelComparator); 187 int N = channels.size(); 188 for (int i = 0; i < N; i++) { 189 final NotificationChannel channel = channels.get(i); 190 populateSingleChannelPrefs(groupCategory, channel); 191 } 192 } 193 194 int deletedChannelCount = mBackend.getDeletedChannelCount(mAppRow.pkg, mAppRow.uid); 195 if (deletedChannelCount > 0 && 196 getPreferenceScreen().findPreference(KEY_DELETED) == null) { 197 mDeletedChannels = new FooterPreference(getPrefContext()); 198 mDeletedChannels.setSelectable(false); 199 mDeletedChannels.setTitle(getResources().getQuantityString( 200 R.plurals.deleted_channels, deletedChannelCount, deletedChannelCount)); 201 mDeletedChannels.setEnabled(false); 202 mDeletedChannels.setKey(KEY_DELETED); 203 mDeletedChannels.setOrder(ORDER_LAST); 204 getPreferenceScreen().addPreference(mDeletedChannels); 205 } 206 } 207 208 updateDependents(mAppRow.banned); 209 } 210 211 private void populateSingleChannelPrefs(PreferenceCategory groupCategory, 212 final NotificationChannel channel) { 213 MasterSwitchPreference channelPref = new MasterSwitchPreference( 214 getPrefContext()); 215 channelPref.setSwitchEnabled(mSuspendedAppsAdmin == null 216 && isChannelBlockable(mAppRow.systemApp, channel) 217 && isChannelConfigurable(channel)); 218 channelPref.setKey(channel.getId()); 219 channelPref.setTitle(channel.getName()); 220 channelPref.setChecked(channel.getImportance() != IMPORTANCE_NONE); 221 channelPref.setSummary(getImportanceSummary(channel)); 222 Bundle channelArgs = new Bundle(); 223 channelArgs.putInt(AppInfoBase.ARG_PACKAGE_UID, mUid); 224 channelArgs.putString(AppInfoBase.ARG_PACKAGE_NAME, mPkg); 225 channelArgs.putString(Settings.EXTRA_CHANNEL_ID, channel.getId()); 226 Intent channelIntent = Utils.onBuildStartFragmentIntent(getActivity(), 227 ChannelNotificationSettings.class.getName(), 228 channelArgs, null, R.string.notification_channel_title, null, false, 229 getMetricsCategory()); 230 channelPref.setIntent(channelIntent); 231 232 channelPref.setOnPreferenceChangeListener( 233 new Preference.OnPreferenceChangeListener() { 234 @Override 235 public boolean onPreferenceChange(Preference preference, 236 Object o) { 237 boolean value = (Boolean) o; 238 int importance = value ? IMPORTANCE_LOW : IMPORTANCE_NONE; 239 channel.setImportance(importance); 240 channel.lockFields( 241 NotificationChannel.USER_LOCKED_IMPORTANCE); 242 channelPref.setSummary(getImportanceSummary(channel)); 243 mBackend.updateChannel(mPkg, mUid, channel); 244 245 return true; 246 } 247 }); 248 groupCategory.addPreference(channelPref); 249 } 250 251 void setupBadge() { 252 mBadge = (RestrictedSwitchPreference) getPreferenceScreen().findPreference(KEY_BADGE); 253 mBadge.setDisabledByAdmin(mSuspendedAppsAdmin); 254 if (mChannel == null) { 255 mBadge.setChecked(mAppRow.showBadge); 256 } else { 257 mBadge.setChecked(mChannel.canShowBadge()); 258 } 259 mBadge.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { 260 @Override 261 public boolean onPreferenceChange(Preference preference, Object newValue) { 262 final boolean value = (Boolean) newValue; 263 if (mChannel == null) { 264 mBackend.setShowBadge(mPkg, mUid, value); 265 } else { 266 mChannel.setShowBadge(value); 267 mChannel.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); 268 mBackend.updateChannel(mPkg, mUid, mChannel); 269 } 270 return true; 271 } 272 }); 273 } 274 275 protected void setupBlock() { 276 View switchBarContainer = LayoutInflater.from( 277 getPrefContext()).inflate(R.layout.styled_switch_bar, null); 278 mSwitchBar = switchBarContainer.findViewById(R.id.switch_bar); 279 mSwitchBar.show(); 280 mSwitchBar.setDisabledByAdmin(mSuspendedAppsAdmin); 281 mSwitchBar.setChecked(!mAppRow.banned); 282 mSwitchBar.addOnSwitchChangeListener(new SwitchBar.OnSwitchChangeListener() { 283 @Override 284 public void onSwitchChanged(Switch switchView, boolean isChecked) { 285 if (mShowLegacyChannelConfig && mChannel != null) { 286 final int importance = isChecked ? IMPORTANCE_UNSPECIFIED : IMPORTANCE_NONE; 287 mImportanceToggle.setChecked(importance == IMPORTANCE_UNSPECIFIED); 288 mChannel.setImportance(importance); 289 mChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 290 mBackend.updateChannel(mPkg, mUid, mChannel); 291 } 292 mBackend.setNotificationsEnabledForPackage(mPkgInfo.packageName, mUid, isChecked); 293 mAppRow.banned = true; 294 updateDependents(!isChecked); 295 } 296 }); 297 298 mBlockBar = new LayoutPreference(getPrefContext(), switchBarContainer); 299 mBlockBar.setOrder(ORDER_FIRST); 300 mBlockBar.setKey(KEY_BLOCK); 301 getPreferenceScreen().addPreference(mBlockBar); 302 303 if (mAppRow.systemApp && !mAppRow.banned) { 304 setVisible(mBlockBar, false); 305 } 306 307 setupBlockDesc(R.string.app_notifications_off_desc); 308 } 309 310 protected void updateDependents(boolean banned) { 311 for (PreferenceCategory category : mChannelGroups) { 312 setVisible(category, !banned); 313 } 314 if (mDeletedChannels != null) { 315 setVisible(mDeletedChannels, !banned); 316 } 317 setVisible(mBlockedDesc, banned); 318 setVisible(mBadge, !banned); 319 if (mShowLegacyChannelConfig) { 320 setVisible(mImportanceToggle, !banned); 321 setVisible(mPriority, checkCanBeVisible(NotificationManager.IMPORTANCE_DEFAULT) 322 || (checkCanBeVisible(NotificationManager.IMPORTANCE_LOW) 323 && mDndVisualEffectsSuppressed)); 324 setVisible(mVisibilityOverride, !banned && 325 checkCanBeVisible(NotificationManager.IMPORTANCE_LOW) && isLockScreenSecure()); 326 } 327 if (mAppLink != null) { 328 setVisible(mAppLink, !banned); 329 } 330 if (mAppRow.systemApp && !mAppRow.banned) { 331 setVisible(mBlockBar, false); 332 } 333 } 334 335 private String getImportanceSummary(NotificationChannel channel) { 336 switch (channel.getImportance()) { 337 case NotificationManager.IMPORTANCE_UNSPECIFIED: 338 return getContext().getString(R.string.notification_importance_unspecified); 339 case NotificationManager.IMPORTANCE_NONE: 340 return getContext().getString(R.string.notification_toggle_off); 341 case NotificationManager.IMPORTANCE_MIN: 342 return getContext().getString(R.string.notification_importance_min); 343 case NotificationManager.IMPORTANCE_LOW: 344 return getContext().getString(R.string.notification_importance_low); 345 case NotificationManager.IMPORTANCE_DEFAULT: 346 if (hasValidSound(channel)) { 347 return getContext().getString(R.string.notification_importance_default); 348 } else { // Silent 349 return getContext().getString(R.string.notification_importance_low); 350 } 351 case NotificationManager.IMPORTANCE_HIGH: 352 case NotificationManager.IMPORTANCE_MAX: 353 default: 354 if (hasValidSound(channel)) { 355 return getContext().getString(R.string.notification_importance_high); 356 } else { // Silent 357 return getContext().getString(R.string.notification_importance_high_silent); 358 } 359 } 360 } 361 362 private Comparator<NotificationChannel> mChannelComparator = 363 new Comparator<NotificationChannel>() { 364 365 @Override 366 public int compare(NotificationChannel left, NotificationChannel right) { 367 if (left.isDeleted() != right.isDeleted()) { 368 return Boolean.compare(left.isDeleted(), right.isDeleted()); 369 } 370 return left.getId().compareTo(right.getId()); 371 } 372 }; 373 374 private Comparator<NotificationChannelGroup> mChannelGroupComparator = 375 new Comparator<NotificationChannelGroup>() { 376 377 @Override 378 public int compare(NotificationChannelGroup left, NotificationChannelGroup right) { 379 // Non-grouped channels (in placeholder group with a null id) come last 380 if (left.getId() == null && right.getId() != null) { 381 return 1; 382 } else if (right.getId() == null && left.getId() != null) { 383 return -1; 384 } 385 return left.getId().compareTo(right.getId()); 386 } 387 }; 388 } 389