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