RankingHelper.java revision 7ddf54211f35ef9cf203154db36148d659b53a7d
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 */ 16package com.android.server.notification; 17 18import com.android.internal.R; 19import com.android.internal.annotations.VisibleForTesting; 20import com.android.internal.logging.MetricsLogger; 21import com.android.internal.logging.nano.MetricsProto; 22import com.android.internal.util.Preconditions; 23 24import android.app.Notification; 25import android.app.NotificationChannel; 26import android.app.NotificationChannelGroup; 27import android.app.NotificationManager; 28import android.content.Context; 29import android.content.pm.ApplicationInfo; 30import android.content.pm.PackageManager; 31import android.content.pm.PackageManager.NameNotFoundException; 32import android.content.pm.ParceledListSlice; 33import android.metrics.LogMaker; 34import android.os.Build; 35import android.os.UserHandle; 36import android.provider.Settings.Secure; 37import android.service.notification.NotificationListenerService.Ranking; 38import android.text.TextUtils; 39import android.util.ArrayMap; 40import android.util.Slog; 41import android.util.SparseBooleanArray; 42 43import org.json.JSONArray; 44import org.json.JSONException; 45import org.json.JSONObject; 46import org.xmlpull.v1.XmlPullParser; 47import org.xmlpull.v1.XmlPullParserException; 48import org.xmlpull.v1.XmlSerializer; 49 50import java.io.IOException; 51import java.io.PrintWriter; 52import java.util.ArrayList; 53import java.util.Arrays; 54import java.util.Collection; 55import java.util.Collections; 56import java.util.List; 57import java.util.Map; 58import java.util.Map.Entry; 59import java.util.Objects; 60 61public class RankingHelper implements RankingConfig { 62 private static final String TAG = "RankingHelper"; 63 64 private static final int XML_VERSION = 1; 65 66 private static final String TAG_RANKING = "ranking"; 67 private static final String TAG_PACKAGE = "package"; 68 private static final String TAG_CHANNEL = "channel"; 69 private static final String TAG_GROUP = "channelGroup"; 70 71 private static final String ATT_VERSION = "version"; 72 private static final String ATT_NAME = "name"; 73 private static final String ATT_UID = "uid"; 74 private static final String ATT_ID = "id"; 75 private static final String ATT_PRIORITY = "priority"; 76 private static final String ATT_VISIBILITY = "visibility"; 77 private static final String ATT_IMPORTANCE = "importance"; 78 private static final String ATT_SHOW_BADGE = "show_badge"; 79 80 private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; 81 private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE; 82 private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED; 83 private static final boolean DEFAULT_SHOW_BADGE = true; 84 85 private final NotificationSignalExtractor[] mSignalExtractors; 86 private final NotificationComparator mPreliminaryComparator; 87 private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator(); 88 89 private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record 90 private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>(); 91 private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record 92 93 private final Context mContext; 94 private final RankingHandler mRankingHandler; 95 private final PackageManager mPm; 96 private SparseBooleanArray mBadgingEnabled; 97 98 public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler, 99 NotificationUsageStats usageStats, String[] extractorNames) { 100 mContext = context; 101 mRankingHandler = rankingHandler; 102 mPm = pm; 103 104 mPreliminaryComparator = new NotificationComparator(mContext); 105 106 updateBadgingEnabled(); 107 108 final int N = extractorNames.length; 109 mSignalExtractors = new NotificationSignalExtractor[N]; 110 for (int i = 0; i < N; i++) { 111 try { 112 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]); 113 NotificationSignalExtractor extractor = 114 (NotificationSignalExtractor) extractorClass.newInstance(); 115 extractor.initialize(mContext, usageStats); 116 extractor.setConfig(this); 117 mSignalExtractors[i] = extractor; 118 } catch (ClassNotFoundException e) { 119 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e); 120 } catch (InstantiationException e) { 121 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e); 122 } catch (IllegalAccessException e) { 123 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e); 124 } 125 } 126 } 127 128 @SuppressWarnings("unchecked") 129 public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) { 130 final int N = mSignalExtractors.length; 131 for (int i = 0; i < N; i++) { 132 final NotificationSignalExtractor extractor = mSignalExtractors[i]; 133 if (extractorClass.equals(extractor.getClass())) { 134 return (T) extractor; 135 } 136 } 137 return null; 138 } 139 140 public void extractSignals(NotificationRecord r) { 141 final int N = mSignalExtractors.length; 142 for (int i = 0; i < N; i++) { 143 NotificationSignalExtractor extractor = mSignalExtractors[i]; 144 try { 145 RankingReconsideration recon = extractor.process(r); 146 if (recon != null) { 147 mRankingHandler.requestReconsideration(recon); 148 } 149 } catch (Throwable t) { 150 Slog.w(TAG, "NotificationSignalExtractor failed.", t); 151 } 152 } 153 } 154 155 public void readXml(XmlPullParser parser, boolean forRestore) 156 throws XmlPullParserException, IOException { 157 int type = parser.getEventType(); 158 if (type != XmlPullParser.START_TAG) return; 159 String tag = parser.getName(); 160 if (!TAG_RANKING.equals(tag)) return; 161 // Clobber groups and channels with the xml, but don't delete other data that wasn't present 162 // at the time of serialization. 163 mRestoredWithoutUids.clear(); 164 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 165 tag = parser.getName(); 166 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 167 return; 168 } 169 if (type == XmlPullParser.START_TAG) { 170 if (TAG_PACKAGE.equals(tag)) { 171 int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID); 172 String name = parser.getAttributeValue(null, ATT_NAME); 173 if (!TextUtils.isEmpty(name)) { 174 if (forRestore) { 175 try { 176 //TODO: http://b/22388012 177 uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM); 178 } catch (NameNotFoundException e) { 179 // noop 180 } 181 } 182 183 Record r = getOrCreateRecord(name, uid, 184 safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE), 185 safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY), 186 safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY), 187 safeBool(parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE)); 188 r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); 189 r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); 190 r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); 191 r.showBadge = safeBool(parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); 192 193 final int innerDepth = parser.getDepth(); 194 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 195 && (type != XmlPullParser.END_TAG 196 || parser.getDepth() > innerDepth)) { 197 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 198 continue; 199 } 200 201 String tagName = parser.getName(); 202 // Channel groups 203 if (TAG_GROUP.equals(tagName)) { 204 String id = parser.getAttributeValue(null, ATT_ID); 205 CharSequence groupName = parser.getAttributeValue(null, ATT_NAME); 206 if (!TextUtils.isEmpty(id)) { 207 NotificationChannelGroup group 208 = new NotificationChannelGroup(id, groupName); 209 r.groups.put(id, group); 210 } 211 } 212 // Channels 213 if (TAG_CHANNEL.equals(tagName)) { 214 String id = parser.getAttributeValue(null, ATT_ID); 215 String channelName = parser.getAttributeValue(null, ATT_NAME); 216 int channelImportance = 217 safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); 218 if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) { 219 NotificationChannel channel = new NotificationChannel(id, 220 channelName, channelImportance); 221 channel.populateFromXml(parser); 222 r.channels.put(id, channel); 223 } 224 } 225 } 226 227 try { 228 deleteDefaultChannelIfNeeded(r); 229 } catch (NameNotFoundException e) { 230 Slog.e(TAG, "deleteDefaultChannelIfNeeded - Exception: " + e); 231 } 232 } 233 } 234 } 235 } 236 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 237 } 238 239 private static String recordKey(String pkg, int uid) { 240 return pkg + "|" + uid; 241 } 242 243 private Record getRecord(String pkg, int uid) { 244 final String key = recordKey(pkg, uid); 245 synchronized (mRecords) { 246 return mRecords.get(key); 247 } 248 } 249 250 private Record getOrCreateRecord(String pkg, int uid) { 251 return getOrCreateRecord(pkg, uid, 252 DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE); 253 } 254 255 private Record getOrCreateRecord(String pkg, int uid, int importance, int priority, 256 int visibility, boolean showBadge) { 257 final String key = recordKey(pkg, uid); 258 synchronized (mRecords) { 259 Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get( 260 key); 261 if (r == null) { 262 r = new Record(); 263 r.pkg = pkg; 264 r.uid = uid; 265 r.importance = importance; 266 r.priority = priority; 267 r.visibility = visibility; 268 r.showBadge = showBadge; 269 270 try { 271 createDefaultChannelIfNeeded(r); 272 } catch (NameNotFoundException e) { 273 Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e); 274 } 275 276 if (r.uid == Record.UNKNOWN_UID) { 277 mRestoredWithoutUids.put(pkg, r); 278 } else { 279 mRecords.put(key, r); 280 } 281 } 282 return r; 283 } 284 } 285 286 private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException { 287 final int userId = UserHandle.getUserId(r.uid); 288 final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId); 289 if (applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) { 290 // O apps should not have the default channel. 291 return false; 292 } 293 294 // Otherwise, this app should have the default channel. 295 return true; 296 } 297 298 private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException { 299 if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { 300 // Not present 301 return; 302 } 303 304 if (shouldHaveDefaultChannel(r)) { 305 // Keep the default channel until upgraded. 306 return; 307 } 308 309 // Remove Default Channel. 310 r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID); 311 } 312 313 private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException { 314 if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { 315 r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID).setName( 316 mContext.getString(R.string.default_notification_channel_label)); 317 return; 318 } 319 320 if (!shouldHaveDefaultChannel(r)) { 321 // Keep the default channel until upgraded. 322 return; 323 } 324 325 // Create Default Channel 326 NotificationChannel channel; 327 channel = new NotificationChannel( 328 NotificationChannel.DEFAULT_CHANNEL_ID, 329 mContext.getString(R.string.default_notification_channel_label), 330 r.importance); 331 channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); 332 channel.setLockscreenVisibility(r.visibility); 333 if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) { 334 channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 335 } 336 if (r.priority != DEFAULT_PRIORITY) { 337 channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); 338 } 339 if (r.visibility != DEFAULT_VISIBILITY) { 340 channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); 341 } 342 r.channels.put(channel.getId(), channel); 343 } 344 345 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 346 out.startTag(null, TAG_RANKING); 347 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 348 349 synchronized (mRecords) { 350 final int N = mRecords.size(); 351 for (int i = 0; i < N; i++) { 352 final Record r = mRecords.valueAt(i); 353 //TODO: http://b/22388012 354 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 355 continue; 356 } 357 final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE 358 || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY 359 || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0 360 || r.groups.size() > 0; 361 if (hasNonDefaultSettings) { 362 out.startTag(null, TAG_PACKAGE); 363 out.attribute(null, ATT_NAME, r.pkg); 364 if (r.importance != DEFAULT_IMPORTANCE) { 365 out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); 366 } 367 if (r.priority != DEFAULT_PRIORITY) { 368 out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); 369 } 370 if (r.visibility != DEFAULT_VISIBILITY) { 371 out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); 372 } 373 out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge)); 374 375 if (!forBackup) { 376 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 377 } 378 379 for (NotificationChannelGroup group : r.groups.values()) { 380 group.writeXml(out); 381 } 382 383 for (NotificationChannel channel : r.channels.values()) { 384 if (!forBackup || (forBackup && !channel.isDeleted())) { 385 channel.writeXml(out); 386 } 387 } 388 389 out.endTag(null, TAG_PACKAGE); 390 } 391 } 392 } 393 out.endTag(null, TAG_RANKING); 394 } 395 396 private void updateConfig() { 397 final int N = mSignalExtractors.length; 398 for (int i = 0; i < N; i++) { 399 mSignalExtractors[i].setConfig(this); 400 } 401 mRankingHandler.requestSort(false); 402 } 403 404 public void sort(ArrayList<NotificationRecord> notificationList) { 405 final int N = notificationList.size(); 406 // clear global sort keys 407 for (int i = N - 1; i >= 0; i--) { 408 notificationList.get(i).setGlobalSortKey(null); 409 } 410 411 // rank each record individually 412 Collections.sort(notificationList, mPreliminaryComparator); 413 414 synchronized (mProxyByGroupTmp) { 415 // record individual ranking result and nominate proxies for each group 416 for (int i = N - 1; i >= 0; i--) { 417 final NotificationRecord record = notificationList.get(i); 418 record.setAuthoritativeRank(i); 419 final String groupKey = record.getGroupKey(); 420 NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey); 421 if (existingProxy == null 422 || record.getImportance() > existingProxy.getImportance()) { 423 mProxyByGroupTmp.put(groupKey, record); 424 } 425 } 426 // assign global sort key: 427 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 428 for (int i = 0; i < N; i++) { 429 final NotificationRecord record = notificationList.get(i); 430 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 431 String groupSortKey = record.getNotification().getSortKey(); 432 433 // We need to make sure the developer provided group sort key (gsk) is handled 434 // correctly: 435 // gsk="" < gsk=non-null-string < gsk=null 436 // 437 // We enforce this by using different prefixes for these three cases. 438 String groupSortKeyPortion; 439 if (groupSortKey == null) { 440 groupSortKeyPortion = "nsk"; 441 } else if (groupSortKey.equals("")) { 442 groupSortKeyPortion = "esk"; 443 } else { 444 groupSortKeyPortion = "gsk=" + groupSortKey; 445 } 446 447 boolean isGroupSummary = record.getNotification().isGroupSummary(); 448 record.setGlobalSortKey( 449 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 450 record.isRecentlyIntrusive() 451 && record.getImportance() > NotificationManager.IMPORTANCE_MIN 452 ? '0' : '1', 453 groupProxy.getAuthoritativeRank(), 454 isGroupSummary ? '0' : '1', 455 groupSortKeyPortion, 456 record.getAuthoritativeRank())); 457 } 458 mProxyByGroupTmp.clear(); 459 } 460 461 // Do a second ranking pass, using group proxies 462 Collections.sort(notificationList, mFinalComparator); 463 } 464 465 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 466 return Collections.binarySearch(notificationList, target, mFinalComparator); 467 } 468 469 private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { 470 final String value = parser.getAttributeValue(null, att); 471 if (TextUtils.isEmpty(value)) return defValue; 472 return Boolean.parseBoolean(value); 473 } 474 475 private static int safeInt(XmlPullParser parser, String att, int defValue) { 476 final String val = parser.getAttributeValue(null, att); 477 return tryParseInt(val, defValue); 478 } 479 480 private static int tryParseInt(String value, int defValue) { 481 if (TextUtils.isEmpty(value)) return defValue; 482 try { 483 return Integer.parseInt(value); 484 } catch (NumberFormatException e) { 485 return defValue; 486 } 487 } 488 489 /** 490 * Gets importance. 491 */ 492 @Override 493 public int getImportance(String packageName, int uid) { 494 return getOrCreateRecord(packageName, uid).importance; 495 } 496 497 @Override 498 public boolean canShowBadge(String packageName, int uid) { 499 return getOrCreateRecord(packageName, uid).showBadge; 500 } 501 502 @Override 503 public void setShowBadge(String packageName, int uid, boolean showBadge) { 504 getOrCreateRecord(packageName, uid).showBadge = showBadge; 505 updateConfig(); 506 } 507 508 int getPackagePriority(String pkg, int uid) { 509 return getOrCreateRecord(pkg, uid).priority; 510 } 511 512 int getPackageVisibility(String pkg, int uid) { 513 return getOrCreateRecord(pkg, uid).visibility; 514 } 515 516 @Override 517 public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, 518 boolean fromTargetApp) { 519 Preconditions.checkNotNull(pkg); 520 Preconditions.checkNotNull(group); 521 Preconditions.checkNotNull(group.getId()); 522 Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName())); 523 Record r = getOrCreateRecord(pkg, uid); 524 if (r == null) { 525 throw new IllegalArgumentException("Invalid package"); 526 } 527 LogMaker lm = new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP) 528 .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) 529 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID, 530 group.getId()) 531 .setPackageName(pkg); 532 MetricsLogger.action(lm); 533 r.groups.put(group.getId(), group); 534 updateConfig(); 535 } 536 537 @Override 538 public void createNotificationChannel(String pkg, int uid, NotificationChannel channel, 539 boolean fromTargetApp) { 540 Preconditions.checkNotNull(pkg); 541 Preconditions.checkNotNull(channel); 542 Preconditions.checkNotNull(channel.getId()); 543 Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())); 544 Record r = getOrCreateRecord(pkg, uid); 545 if (r == null) { 546 throw new IllegalArgumentException("Invalid package"); 547 } 548 if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) { 549 throw new IllegalArgumentException("NotificationChannelGroup doesn't exist"); 550 } 551 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) { 552 throw new IllegalArgumentException("Reserved id"); 553 } 554 555 NotificationChannel existing = r.channels.get(channel.getId()); 556 // Keep most of the existing settings 557 if (existing != null && fromTargetApp) { 558 if (existing.isDeleted()) { 559 existing.setDeleted(false); 560 } 561 562 existing.setName(channel.getName().toString()); 563 existing.setDescription(channel.getDescription()); 564 existing.setBlockableSystem(channel.isBlockableSystem()); 565 566 MetricsLogger.action(getChannelLog(channel, pkg)); 567 updateConfig(); 568 return; 569 } 570 if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE 571 || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) { 572 throw new IllegalArgumentException("Invalid importance level"); 573 } 574 // Reset fields that apps aren't allowed to set. 575 if (fromTargetApp) { 576 channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); 577 channel.setLockscreenVisibility(r.visibility); 578 } 579 clearLockedFields(channel); 580 if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 581 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 582 } 583 if (!r.showBadge) { 584 channel.setShowBadge(false); 585 } 586 r.channels.put(channel.getId(), channel); 587 MetricsLogger.action(getChannelLog(channel, pkg).setType( 588 MetricsProto.MetricsEvent.TYPE_OPEN)); 589 updateConfig(); 590 } 591 592 void clearLockedFields(NotificationChannel channel) { 593 channel.unlockFields(channel.getUserLockedFields()); 594 } 595 596 @Override 597 public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) { 598 Preconditions.checkNotNull(updatedChannel); 599 Preconditions.checkNotNull(updatedChannel.getId()); 600 Record r = getOrCreateRecord(pkg, uid); 601 if (r == null) { 602 throw new IllegalArgumentException("Invalid package"); 603 } 604 NotificationChannel channel = r.channels.get(updatedChannel.getId()); 605 if (channel == null || channel.isDeleted()) { 606 throw new IllegalArgumentException("Channel does not exist"); 607 } 608 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 609 updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 610 } 611 lockFieldsForUpdate(channel, updatedChannel); 612 r.channels.put(updatedChannel.getId(), updatedChannel); 613 614 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(updatedChannel.getId())) { 615 // copy settings to app level so they are inherited by new channels 616 // when the app migrates 617 r.importance = updatedChannel.getImportance(); 618 r.priority = updatedChannel.canBypassDnd() 619 ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT; 620 r.visibility = updatedChannel.getLockscreenVisibility(); 621 r.showBadge = updatedChannel.canShowBadge(); 622 } 623 624 MetricsLogger.action(getChannelLog(updatedChannel, pkg)); 625 updateConfig(); 626 } 627 628 @Override 629 public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, 630 boolean includeDeleted) { 631 Preconditions.checkNotNull(pkg); 632 Record r = getOrCreateRecord(pkg, uid); 633 if (r == null) { 634 return null; 635 } 636 if (channelId == null) { 637 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 638 } 639 final NotificationChannel nc = r.channels.get(channelId); 640 if (nc != null && (includeDeleted || !nc.isDeleted())) { 641 return nc; 642 } 643 return null; 644 } 645 646 @Override 647 public void deleteNotificationChannel(String pkg, int uid, String channelId) { 648 Record r = getRecord(pkg, uid); 649 if (r == null) { 650 return; 651 } 652 NotificationChannel channel = r.channels.get(channelId); 653 if (channel != null) { 654 channel.setDeleted(true); 655 LogMaker lm = getChannelLog(channel, pkg); 656 lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE); 657 MetricsLogger.action(lm); 658 updateConfig(); 659 } 660 } 661 662 @Override 663 @VisibleForTesting 664 public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { 665 Preconditions.checkNotNull(pkg); 666 Preconditions.checkNotNull(channelId); 667 Record r = getRecord(pkg, uid); 668 if (r == null) { 669 return; 670 } 671 r.channels.remove(channelId); 672 updateConfig(); 673 } 674 675 @Override 676 public void permanentlyDeleteNotificationChannels(String pkg, int uid) { 677 Preconditions.checkNotNull(pkg); 678 Record r = getRecord(pkg, uid); 679 if (r == null) { 680 return; 681 } 682 int N = r.channels.size() - 1; 683 for (int i = N; i >= 0; i--) { 684 String key = r.channels.keyAt(i); 685 if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { 686 r.channels.remove(key); 687 } 688 } 689 updateConfig(); 690 } 691 692 public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, 693 int uid) { 694 Preconditions.checkNotNull(pkg); 695 Record r = getRecord(pkg, uid); 696 return r.groups.get(groupId); 697 } 698 699 @Override 700 public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 701 int uid, boolean includeDeleted) { 702 Preconditions.checkNotNull(pkg); 703 Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); 704 Record r = getRecord(pkg, uid); 705 if (r == null) { 706 return ParceledListSlice.emptyList(); 707 } 708 NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); 709 int N = r.channels.size(); 710 for (int i = 0; i < N; i++) { 711 final NotificationChannel nc = r.channels.valueAt(i); 712 if (includeDeleted || !nc.isDeleted()) { 713 if (nc.getGroup() != null) { 714 if (r.groups.get(nc.getGroup()) != null) { 715 NotificationChannelGroup ncg = groups.get(nc.getGroup()); 716 if (ncg == null) { 717 ncg = r.groups.get(nc.getGroup()).clone(); 718 groups.put(nc.getGroup(), ncg); 719 720 } 721 ncg.addChannel(nc); 722 } 723 } else { 724 nonGrouped.addChannel(nc); 725 } 726 } 727 } 728 if (nonGrouped.getChannels().size() > 0) { 729 groups.put(null, nonGrouped); 730 } 731 return new ParceledListSlice<>(new ArrayList<>(groups.values())); 732 } 733 734 public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid, 735 String groupId) { 736 List<NotificationChannel> deletedChannels = new ArrayList<>(); 737 Record r = getRecord(pkg, uid); 738 if (r == null || TextUtils.isEmpty(groupId)) { 739 return deletedChannels; 740 } 741 742 r.groups.remove(groupId); 743 744 int N = r.channels.size(); 745 for (int i = 0; i < N; i++) { 746 final NotificationChannel nc = r.channels.valueAt(i); 747 if (groupId.equals(nc.getGroup())) { 748 nc.setDeleted(true); 749 deletedChannels.add(nc); 750 } 751 } 752 updateConfig(); 753 return deletedChannels; 754 } 755 756 @Override 757 public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 758 int uid) { 759 Record r = getRecord(pkg, uid); 760 if (r == null) { 761 return new ArrayList<>(); 762 } 763 return r.groups.values(); 764 } 765 766 @Override 767 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, 768 boolean includeDeleted) { 769 Preconditions.checkNotNull(pkg); 770 List<NotificationChannel> channels = new ArrayList<>(); 771 Record r = getRecord(pkg, uid); 772 if (r == null) { 773 return ParceledListSlice.emptyList(); 774 } 775 int N = r.channels.size(); 776 for (int i = 0; i < N; i++) { 777 final NotificationChannel nc = r.channels.valueAt(i); 778 if (includeDeleted || !nc.isDeleted()) { 779 channels.add(nc); 780 } 781 } 782 return new ParceledListSlice<>(channels); 783 } 784 785 /** 786 * True for pre-O apps that only have the default channel, or pre O apps that have no 787 * channels yet. This method will create the default channel for pre-O apps that don't have it. 788 * Should never be true for O+ targeting apps, but that's enforced on boot/when an app 789 * upgrades. 790 */ 791 public boolean onlyHasDefaultChannel(String pkg, int uid) { 792 Record r = getOrCreateRecord(pkg, uid); 793 if (r.channels.size() == 1 794 && r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { 795 return true; 796 } 797 return false; 798 } 799 800 public int getDeletedChannelCount(String pkg, int uid) { 801 Preconditions.checkNotNull(pkg); 802 int deletedCount = 0; 803 Record r = getRecord(pkg, uid); 804 if (r == null) { 805 return deletedCount; 806 } 807 int N = r.channels.size(); 808 for (int i = 0; i < N; i++) { 809 final NotificationChannel nc = r.channels.valueAt(i); 810 if (nc.isDeleted()) { 811 deletedCount++; 812 } 813 } 814 return deletedCount; 815 } 816 817 /** 818 * Sets importance. 819 */ 820 @Override 821 public void setImportance(String pkgName, int uid, int importance) { 822 getOrCreateRecord(pkgName, uid).importance = importance; 823 updateConfig(); 824 } 825 826 public void setEnabled(String packageName, int uid, boolean enabled) { 827 boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE; 828 if (wasEnabled == enabled) { 829 return; 830 } 831 setImportance(packageName, uid, 832 enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE); 833 } 834 835 @VisibleForTesting 836 void lockFieldsForUpdate(NotificationChannel original, NotificationChannel update) { 837 update.unlockFields(update.getUserLockedFields()); 838 update.lockFields(original.getUserLockedFields()); 839 if (original.canBypassDnd() != update.canBypassDnd()) { 840 update.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); 841 } 842 if (original.getLockscreenVisibility() != update.getLockscreenVisibility()) { 843 update.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); 844 } 845 if (original.getImportance() != update.getImportance()) { 846 update.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 847 } 848 if (original.shouldShowLights() != update.shouldShowLights() 849 || original.getLightColor() != update.getLightColor()) { 850 update.lockFields(NotificationChannel.USER_LOCKED_LIGHTS); 851 } 852 if (!Objects.equals(original.getSound(), update.getSound())) { 853 update.lockFields(NotificationChannel.USER_LOCKED_SOUND); 854 } 855 if (!Arrays.equals(original.getVibrationPattern(), update.getVibrationPattern()) 856 || original.shouldVibrate() != update.shouldVibrate()) { 857 update.lockFields(NotificationChannel.USER_LOCKED_VIBRATION); 858 } 859 if (original.canShowBadge() != update.canShowBadge()) { 860 update.lockFields(NotificationChannel.USER_LOCKED_SHOW_BADGE); 861 } 862 } 863 864 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 865 if (filter == null) { 866 final int N = mSignalExtractors.length; 867 pw.print(prefix); 868 pw.print("mSignalExtractors.length = "); 869 pw.println(N); 870 for (int i = 0; i < N; i++) { 871 pw.print(prefix); 872 pw.print(" "); 873 pw.println(mSignalExtractors[i]); 874 } 875 } 876 if (filter == null) { 877 pw.print(prefix); 878 pw.println("per-package config:"); 879 } 880 pw.println("Records:"); 881 synchronized (mRecords) { 882 dumpRecords(pw, prefix, filter, mRecords); 883 } 884 pw.println("Restored without uid:"); 885 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 886 } 887 888 private static void dumpRecords(PrintWriter pw, String prefix, 889 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 890 final int N = records.size(); 891 for (int i = 0; i < N; i++) { 892 final Record r = records.valueAt(i); 893 if (filter == null || filter.matches(r.pkg)) { 894 pw.print(prefix); 895 pw.print(" AppSettings: "); 896 pw.print(r.pkg); 897 pw.print(" ("); 898 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 899 pw.print(')'); 900 if (r.importance != DEFAULT_IMPORTANCE) { 901 pw.print(" importance="); 902 pw.print(Ranking.importanceToString(r.importance)); 903 } 904 if (r.priority != DEFAULT_PRIORITY) { 905 pw.print(" priority="); 906 pw.print(Notification.priorityToString(r.priority)); 907 } 908 if (r.visibility != DEFAULT_VISIBILITY) { 909 pw.print(" visibility="); 910 pw.print(Notification.visibilityToString(r.visibility)); 911 } 912 pw.print(" showBadge="); 913 pw.print(Boolean.toString(r.showBadge)); 914 pw.println(); 915 for (NotificationChannel channel : r.channels.values()) { 916 pw.print(prefix); 917 pw.print(" "); 918 pw.print(" "); 919 pw.println(channel); 920 } 921 for (NotificationChannelGroup group : r.groups.values()) { 922 pw.print(prefix); 923 pw.print(" "); 924 pw.print(" "); 925 pw.println(group); 926 } 927 } 928 } 929 } 930 931 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 932 JSONObject ranking = new JSONObject(); 933 JSONArray records = new JSONArray(); 934 try { 935 ranking.put("noUid", mRestoredWithoutUids.size()); 936 } catch (JSONException e) { 937 // pass 938 } 939 synchronized (mRecords) { 940 final int N = mRecords.size(); 941 for (int i = 0; i < N; i++) { 942 final Record r = mRecords.valueAt(i); 943 if (filter == null || filter.matches(r.pkg)) { 944 JSONObject record = new JSONObject(); 945 try { 946 record.put("userId", UserHandle.getUserId(r.uid)); 947 record.put("packageName", r.pkg); 948 if (r.importance != DEFAULT_IMPORTANCE) { 949 record.put("importance", Ranking.importanceToString(r.importance)); 950 } 951 if (r.priority != DEFAULT_PRIORITY) { 952 record.put("priority", Notification.priorityToString(r.priority)); 953 } 954 if (r.visibility != DEFAULT_VISIBILITY) { 955 record.put("visibility", Notification.visibilityToString(r.visibility)); 956 } 957 if (r.showBadge != DEFAULT_SHOW_BADGE) { 958 record.put("showBadge", Boolean.valueOf(r.showBadge)); 959 } 960 for (NotificationChannel channel : r.channels.values()) { 961 record.put("channel", channel.toJson()); 962 } 963 for (NotificationChannelGroup group : r.groups.values()) { 964 record.put("group", group.toJson()); 965 } 966 } catch (JSONException e) { 967 // pass 968 } 969 records.put(record); 970 } 971 } 972 } 973 try { 974 ranking.put("records", records); 975 } catch (JSONException e) { 976 // pass 977 } 978 return ranking; 979 } 980 981 /** 982 * Dump only the ban information as structured JSON for the stats collector. 983 * 984 * This is intentionally redundant with {#link dumpJson} because the old 985 * scraper will expect this format. 986 * 987 * @param filter 988 * @return 989 */ 990 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 991 JSONArray bans = new JSONArray(); 992 Map<Integer, String> packageBans = getPackageBans(); 993 for(Entry<Integer, String> ban : packageBans.entrySet()) { 994 final int userId = UserHandle.getUserId(ban.getKey()); 995 final String packageName = ban.getValue(); 996 if (filter == null || filter.matches(packageName)) { 997 JSONObject banJson = new JSONObject(); 998 try { 999 banJson.put("userId", userId); 1000 banJson.put("packageName", packageName); 1001 } catch (JSONException e) { 1002 e.printStackTrace(); 1003 } 1004 bans.put(banJson); 1005 } 1006 } 1007 return bans; 1008 } 1009 1010 public Map<Integer, String> getPackageBans() { 1011 synchronized (mRecords) { 1012 final int N = mRecords.size(); 1013 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 1014 for (int i = 0; i < N; i++) { 1015 final Record r = mRecords.valueAt(i); 1016 if (r.importance == NotificationManager.IMPORTANCE_NONE) { 1017 packageBans.put(r.uid, r.pkg); 1018 } 1019 } 1020 1021 return packageBans; 1022 } 1023 } 1024 1025 /** 1026 * Dump only the channel information as structured JSON for the stats collector. 1027 * 1028 * This is intentionally redundant with {#link dumpJson} because the old 1029 * scraper will expect this format. 1030 * 1031 * @param filter 1032 * @return 1033 */ 1034 public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) { 1035 JSONArray channels = new JSONArray(); 1036 Map<String, Integer> packageChannels = getPackageChannels(); 1037 for(Entry<String, Integer> channelCount : packageChannels.entrySet()) { 1038 final String packageName = channelCount.getKey(); 1039 if (filter == null || filter.matches(packageName)) { 1040 JSONObject channelCountJson = new JSONObject(); 1041 try { 1042 channelCountJson.put("packageName", packageName); 1043 channelCountJson.put("channelCount", channelCount.getValue()); 1044 } catch (JSONException e) { 1045 e.printStackTrace(); 1046 } 1047 channels.put(channelCountJson); 1048 } 1049 } 1050 return channels; 1051 } 1052 1053 private Map<String, Integer> getPackageChannels() { 1054 ArrayMap<String, Integer> packageChannels = new ArrayMap<>(); 1055 synchronized (mRecords) { 1056 for (int i = 0; i < mRecords.size(); i++) { 1057 final Record r = mRecords.valueAt(i); 1058 int channelCount = 0; 1059 for (int j = 0; j < r.channels.size(); j++) { 1060 if (!r.channels.valueAt(j).isDeleted()) { 1061 channelCount++; 1062 } 1063 } 1064 packageChannels.put(r.pkg, channelCount); 1065 } 1066 } 1067 return packageChannels; 1068 } 1069 1070 public void onUserRemoved(int userId) { 1071 synchronized (mRecords) { 1072 int N = mRecords.size(); 1073 for (int i = N - 1; i >= 0 ; i--) { 1074 Record record = mRecords.valueAt(i); 1075 if (UserHandle.getUserId(record.uid) == userId) { 1076 mRecords.removeAt(i); 1077 } 1078 } 1079 } 1080 } 1081 1082 public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, 1083 int[] uidList) { 1084 if (pkgList == null || pkgList.length == 0) { 1085 return; // nothing to do 1086 } 1087 boolean updated = false; 1088 if (removingPackage) { 1089 // Remove notification settings for uninstalled package 1090 int size = Math.min(pkgList.length, uidList.length); 1091 for (int i = 0; i < size; i++) { 1092 final String pkg = pkgList[i]; 1093 final int uid = uidList[i]; 1094 synchronized (mRecords) { 1095 mRecords.remove(recordKey(pkg, uid)); 1096 } 1097 mRestoredWithoutUids.remove(pkg); 1098 updated = true; 1099 } 1100 } else { 1101 for (String pkg : pkgList) { 1102 // Package install 1103 final Record r = mRestoredWithoutUids.get(pkg); 1104 if (r != null) { 1105 try { 1106 r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); 1107 mRestoredWithoutUids.remove(pkg); 1108 synchronized (mRecords) { 1109 mRecords.put(recordKey(r.pkg, r.uid), r); 1110 } 1111 updated = true; 1112 } catch (NameNotFoundException e) { 1113 // noop 1114 } 1115 } 1116 // Package upgrade 1117 try { 1118 Record fullRecord = getRecord(pkg, 1119 mPm.getPackageUidAsUser(pkg, changeUserId)); 1120 if (fullRecord != null) { 1121 createDefaultChannelIfNeeded(fullRecord); 1122 deleteDefaultChannelIfNeeded(fullRecord); 1123 } 1124 } catch (NameNotFoundException e) {} 1125 } 1126 } 1127 1128 if (updated) { 1129 updateConfig(); 1130 } 1131 } 1132 1133 private LogMaker getChannelLog(NotificationChannel channel, String pkg) { 1134 return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL) 1135 .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) 1136 .setPackageName(pkg) 1137 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, 1138 channel.getId()) 1139 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, 1140 channel.getImportance()); 1141 } 1142 1143 public void updateBadgingEnabled() { 1144 if (mBadgingEnabled == null) { 1145 mBadgingEnabled = new SparseBooleanArray(); 1146 } 1147 boolean changed = false; 1148 // update the cached values 1149 for (int index = 0; index < mBadgingEnabled.size(); index++) { 1150 int userId = mBadgingEnabled.keyAt(index); 1151 final boolean oldValue = mBadgingEnabled.get(userId); 1152 final boolean newValue = Secure.getIntForUser(mContext.getContentResolver(), 1153 Secure.NOTIFICATION_BADGING, 1154 DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0; 1155 mBadgingEnabled.put(userId, newValue); 1156 changed |= oldValue != newValue; 1157 } 1158 if (changed) { 1159 mRankingHandler.requestSort(false); 1160 } 1161 } 1162 1163 public boolean badgingEnabled(UserHandle userHandle) { 1164 int userId = userHandle.getIdentifier(); 1165 if (userId == UserHandle.USER_ALL) { 1166 return false; 1167 } 1168 if (mBadgingEnabled.indexOfKey(userId) < 0) { 1169 mBadgingEnabled.put(userId, 1170 Secure.getIntForUser(mContext.getContentResolver(), 1171 Secure.NOTIFICATION_BADGING, 1172 DEFAULT_SHOW_BADGE ? 1 : 0, userId) != 0); 1173 } 1174 return mBadgingEnabled.get(userId, DEFAULT_SHOW_BADGE); 1175 } 1176 1177 1178 private static class Record { 1179 static int UNKNOWN_UID = UserHandle.USER_NULL; 1180 1181 String pkg; 1182 int uid = UNKNOWN_UID; 1183 int importance = DEFAULT_IMPORTANCE; 1184 int priority = DEFAULT_PRIORITY; 1185 int visibility = DEFAULT_VISIBILITY; 1186 boolean showBadge = DEFAULT_SHOW_BADGE; 1187 1188 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 1189 ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>(); 1190 } 1191} 1192