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