RankingHelper.java revision 440119679893ee3d4fcccad8ce9faaaf8e54dcad
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 synchronized (mRecords) { 236 return mRecords.get(key); 237 } 238 } 239 240 private Record getOrCreateRecord(String pkg, int uid) { 241 return getOrCreateRecord(pkg, uid, 242 DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE); 243 } 244 245 private Record getOrCreateRecord(String pkg, int uid, int importance, int priority, 246 int visibility, boolean showBadge) { 247 final String key = recordKey(pkg, uid); 248 synchronized (mRecords) { 249 Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get( 250 key); 251 if (r == null) { 252 r = new Record(); 253 r.pkg = pkg; 254 r.uid = uid; 255 r.importance = importance; 256 r.priority = priority; 257 r.visibility = visibility; 258 r.showBadge = showBadge; 259 260 try { 261 createDefaultChannelIfNeeded(r); 262 } catch (NameNotFoundException e) { 263 Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e); 264 } 265 266 if (r.uid == Record.UNKNOWN_UID) { 267 mRestoredWithoutUids.put(pkg, r); 268 } else { 269 mRecords.put(key, r); 270 } 271 } 272 return r; 273 } 274 } 275 276 private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException { 277 final int userId = UserHandle.getUserId(r.uid); 278 final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId); 279 if (applicationInfo.targetSdkVersion <= Build.VERSION_CODES.N_MR1) { 280 // Pre-O apps should have it. 281 return true; 282 } 283 284 // STOPSHIP TODO: remove before release - O+ apps should never have a default channel. 285 // But for now, leave the default channel until an app has created its first channel. 286 boolean hasCreatedAChannel = false; 287 final int size = r.channels.size(); 288 for (int i = 0; i < size; i++) { 289 final NotificationChannel notificationChannel = r.channels.valueAt(i); 290 if (notificationChannel != null && 291 !notificationChannel.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) { 292 hasCreatedAChannel = true; 293 break; 294 } 295 } 296 if (!hasCreatedAChannel) { 297 return true; 298 } 299 300 // Otherwise, should not have the default channel. 301 return false; 302 } 303 304 private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException { 305 if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { 306 // Not present 307 return; 308 } 309 310 if (shouldHaveDefaultChannel(r)) { 311 // Keep the default channel until upgraded. 312 return; 313 } 314 315 // Remove Default Channel. 316 r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID); 317 } 318 319 private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException { 320 if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) { 321 // Already exists 322 return; 323 } 324 325 if (!shouldHaveDefaultChannel(r)) { 326 // Keep the default channel until upgraded. 327 return; 328 } 329 330 // Create Default Channel 331 NotificationChannel channel; 332 channel = new NotificationChannel( 333 NotificationChannel.DEFAULT_CHANNEL_ID, 334 mContext.getString(R.string.default_notification_channel_label), 335 r.importance); 336 channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); 337 channel.setLockscreenVisibility(r.visibility); 338 if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) { 339 channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE); 340 } 341 if (r.priority != DEFAULT_PRIORITY) { 342 channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY); 343 } 344 if (r.visibility != DEFAULT_VISIBILITY) { 345 channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY); 346 } 347 r.channels.put(channel.getId(), channel); 348 } 349 350 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 351 out.startTag(null, TAG_RANKING); 352 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 353 354 synchronized (mRecords) { 355 final int N = mRecords.size(); 356 for (int i = 0; i < N; i++) { 357 final Record r = mRecords.valueAt(i); 358 //TODO: http://b/22388012 359 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 360 continue; 361 } 362 final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE 363 || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY 364 || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0 365 || r.groups.size() > 0; 366 if (hasNonDefaultSettings) { 367 out.startTag(null, TAG_PACKAGE); 368 out.attribute(null, ATT_NAME, r.pkg); 369 if (r.importance != DEFAULT_IMPORTANCE) { 370 out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); 371 } 372 if (r.priority != DEFAULT_PRIORITY) { 373 out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); 374 } 375 if (r.visibility != DEFAULT_VISIBILITY) { 376 out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); 377 } 378 out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge)); 379 380 if (!forBackup) { 381 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 382 } 383 384 for (NotificationChannelGroup group : r.groups.values()) { 385 group.writeXml(out); 386 } 387 388 for (NotificationChannel channel : r.channels.values()) { 389 if (!forBackup || (forBackup && !channel.isDeleted())) { 390 channel.writeXml(out); 391 } 392 } 393 394 out.endTag(null, TAG_PACKAGE); 395 } 396 } 397 } 398 out.endTag(null, TAG_RANKING); 399 } 400 401 private void updateConfig() { 402 final int N = mSignalExtractors.length; 403 for (int i = 0; i < N; i++) { 404 mSignalExtractors[i].setConfig(this); 405 } 406 mRankingHandler.requestSort(false); 407 } 408 409 public void sort(ArrayList<NotificationRecord> notificationList) { 410 final int N = notificationList.size(); 411 // clear global sort keys 412 for (int i = N - 1; i >= 0; i--) { 413 notificationList.get(i).setGlobalSortKey(null); 414 } 415 416 // rank each record individually 417 Collections.sort(notificationList, mPreliminaryComparator); 418 419 synchronized (mProxyByGroupTmp) { 420 // record individual ranking result and nominate proxies for each group 421 for (int i = N - 1; i >= 0; i--) { 422 final NotificationRecord record = notificationList.get(i); 423 record.setAuthoritativeRank(i); 424 final String groupKey = record.getGroupKey(); 425 NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey); 426 if (existingProxy == null 427 || record.getImportance() > existingProxy.getImportance()) { 428 mProxyByGroupTmp.put(groupKey, record); 429 } 430 } 431 // assign global sort key: 432 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 433 for (int i = 0; i < N; i++) { 434 final NotificationRecord record = notificationList.get(i); 435 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 436 String groupSortKey = record.getNotification().getSortKey(); 437 438 // We need to make sure the developer provided group sort key (gsk) is handled 439 // correctly: 440 // gsk="" < gsk=non-null-string < gsk=null 441 // 442 // We enforce this by using different prefixes for these three cases. 443 String groupSortKeyPortion; 444 if (groupSortKey == null) { 445 groupSortKeyPortion = "nsk"; 446 } else if (groupSortKey.equals("")) { 447 groupSortKeyPortion = "esk"; 448 } else { 449 groupSortKeyPortion = "gsk=" + groupSortKey; 450 } 451 452 boolean isGroupSummary = record.getNotification().isGroupSummary(); 453 record.setGlobalSortKey( 454 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 455 record.isRecentlyIntrusive() ? '0' : '1', 456 groupProxy.getAuthoritativeRank(), 457 isGroupSummary ? '0' : '1', 458 groupSortKeyPortion, 459 record.getAuthoritativeRank())); 460 } 461 mProxyByGroupTmp.clear(); 462 } 463 464 // Do a second ranking pass, using group proxies 465 Collections.sort(notificationList, mFinalComparator); 466 } 467 468 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 469 return Collections.binarySearch(notificationList, target, mFinalComparator); 470 } 471 472 private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { 473 final String value = parser.getAttributeValue(null, att); 474 if (TextUtils.isEmpty(value)) return defValue; 475 return Boolean.parseBoolean(value); 476 } 477 478 private static int safeInt(XmlPullParser parser, String att, int defValue) { 479 final String val = parser.getAttributeValue(null, att); 480 return tryParseInt(val, defValue); 481 } 482 483 private static int tryParseInt(String value, int defValue) { 484 if (TextUtils.isEmpty(value)) return defValue; 485 try { 486 return Integer.parseInt(value); 487 } catch (NumberFormatException e) { 488 return defValue; 489 } 490 } 491 492 /** 493 * Gets importance. 494 */ 495 @Override 496 public int getImportance(String packageName, int uid) { 497 return getOrCreateRecord(packageName, uid).importance; 498 } 499 500 @Override 501 public boolean canShowBadge(String packageName, int uid) { 502 return getOrCreateRecord(packageName, uid).showBadge; 503 } 504 505 @Override 506 public void setShowBadge(String packageName, int uid, boolean showBadge) { 507 getOrCreateRecord(packageName, uid).showBadge = showBadge; 508 updateConfig(); 509 } 510 511 @Override 512 public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group, 513 boolean fromTargetApp) { 514 Preconditions.checkNotNull(pkg); 515 Preconditions.checkNotNull(group); 516 Preconditions.checkNotNull(group.getId()); 517 Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName())); 518 Record r = getOrCreateRecord(pkg, uid); 519 if (r == null) { 520 throw new IllegalArgumentException("Invalid package"); 521 } 522 LogMaker lm = new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP) 523 .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) 524 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID, 525 group.getId()) 526 .setPackageName(pkg); 527 MetricsLogger.action(lm); 528 r.groups.put(group.getId(), group); 529 updateConfig(); 530 } 531 532 @Override 533 public void createNotificationChannel(String pkg, int uid, NotificationChannel channel, 534 boolean fromTargetApp) { 535 Preconditions.checkNotNull(pkg); 536 Preconditions.checkNotNull(channel); 537 Preconditions.checkNotNull(channel.getId()); 538 Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())); 539 Record r = getOrCreateRecord(pkg, uid); 540 if (r == null) { 541 throw new IllegalArgumentException("Invalid package"); 542 } 543 if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) { 544 throw new IllegalArgumentException("NotificationChannelGroup doesn't exist"); 545 } 546 if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) { 547 throw new IllegalArgumentException("Reserved id"); 548 } 549 550 NotificationChannel existing = r.channels.get(channel.getId()); 551 // Keep existing settings, except deleted status and name 552 if (existing != null && fromTargetApp) { 553 if (existing.isDeleted()) { 554 existing.setDeleted(false); 555 } 556 557 existing.setName(channel.getName().toString()); 558 existing.setDescription(channel.getDescription()); 559 560 MetricsLogger.action(getChannelLog(channel, pkg)); 561 updateConfig(); 562 return; 563 } 564 if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE 565 || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) { 566 throw new IllegalArgumentException("Invalid importance level"); 567 } 568 // Reset fields that apps aren't allowed to set. 569 if (fromTargetApp) { 570 channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); 571 channel.setLockscreenVisibility(r.visibility); 572 } 573 clearLockedFields(channel); 574 if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 575 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 576 } 577 if (!r.showBadge) { 578 channel.setShowBadge(false); 579 } 580 r.channels.put(channel.getId(), channel); 581 MetricsLogger.action(getChannelLog(channel, pkg).setType( 582 MetricsProto.MetricsEvent.TYPE_OPEN)); 583 updateConfig(); 584 } 585 586 private void clearLockedFields(NotificationChannel channel) { 587 int clearMask = 0; 588 for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { 589 clearMask |= NotificationChannel.LOCKABLE_FIELDS[i]; 590 } 591 channel.lockFields(~clearMask); 592 } 593 594 @Override 595 public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) { 596 Preconditions.checkNotNull(updatedChannel); 597 Preconditions.checkNotNull(updatedChannel.getId()); 598 Record r = getOrCreateRecord(pkg, uid); 599 if (r == null) { 600 throw new IllegalArgumentException("Invalid package"); 601 } 602 NotificationChannel channel = r.channels.get(updatedChannel.getId()); 603 if (channel == null || channel.isDeleted()) { 604 throw new IllegalArgumentException("Channel does not exist"); 605 } 606 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 607 updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 608 } 609 r.channels.put(updatedChannel.getId(), updatedChannel); 610 611 MetricsLogger.action(getChannelLog(updatedChannel, pkg)); 612 updateConfig(); 613 } 614 615 @Override 616 public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, 617 boolean includeDeleted) { 618 Preconditions.checkNotNull(pkg); 619 Record r = getOrCreateRecord(pkg, uid); 620 if (r == null) { 621 return null; 622 } 623 if (channelId == null) { 624 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 625 } 626 final NotificationChannel nc = r.channels.get(channelId); 627 if (nc != null && (includeDeleted || !nc.isDeleted())) { 628 return nc; 629 } 630 return null; 631 } 632 633 @Override 634 public void deleteNotificationChannel(String pkg, int uid, String channelId) { 635 Record r = getRecord(pkg, uid); 636 if (r == null) { 637 return; 638 } 639 NotificationChannel channel = r.channels.get(channelId); 640 if (channel != null) { 641 channel.setDeleted(true); 642 LogMaker lm = getChannelLog(channel, pkg); 643 lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE); 644 MetricsLogger.action(lm); 645 updateConfig(); 646 } 647 } 648 649 @Override 650 @VisibleForTesting 651 public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { 652 Preconditions.checkNotNull(pkg); 653 Preconditions.checkNotNull(channelId); 654 Record r = getRecord(pkg, uid); 655 if (r == null) { 656 return; 657 } 658 r.channels.remove(channelId); 659 updateConfig(); 660 } 661 662 @Override 663 public void permanentlyDeleteNotificationChannels(String pkg, int uid) { 664 Preconditions.checkNotNull(pkg); 665 Record r = getRecord(pkg, uid); 666 if (r == null) { 667 return; 668 } 669 int N = r.channels.size() - 1; 670 for (int i = N; i >= 0; i--) { 671 String key = r.channels.keyAt(i); 672 if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { 673 r.channels.remove(key); 674 } 675 } 676 updateConfig(); 677 } 678 679 public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, 680 int uid) { 681 Preconditions.checkNotNull(pkg); 682 Record r = getRecord(pkg, uid); 683 return r.groups.get(groupId); 684 } 685 686 @Override 687 public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 688 int uid, boolean includeDeleted) { 689 Preconditions.checkNotNull(pkg); 690 Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); 691 Record r = getRecord(pkg, uid); 692 if (r == null) { 693 return ParceledListSlice.emptyList(); 694 } 695 NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); 696 int N = r.channels.size(); 697 for (int i = 0; i < N; i++) { 698 final NotificationChannel nc = r.channels.valueAt(i); 699 if (includeDeleted || !nc.isDeleted()) { 700 if (nc.getGroup() != null) { 701 if (r.groups.get(nc.getGroup()) != null) { 702 NotificationChannelGroup ncg = groups.get(nc.getGroup()); 703 if (ncg == null) { 704 ncg = r.groups.get(nc.getGroup()).clone(); 705 groups.put(nc.getGroup(), ncg); 706 707 } 708 ncg.addChannel(nc); 709 } 710 } else { 711 nonGrouped.addChannel(nc); 712 } 713 } 714 } 715 if (nonGrouped.getChannels().size() > 0) { 716 groups.put(null, nonGrouped); 717 } 718 return new ParceledListSlice<>(new ArrayList<>(groups.values())); 719 } 720 721 public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid, 722 String groupId) { 723 List<NotificationChannel> deletedChannels = new ArrayList<>(); 724 Record r = getRecord(pkg, uid); 725 if (r == null || TextUtils.isEmpty(groupId)) { 726 return deletedChannels; 727 } 728 729 r.groups.remove(groupId); 730 731 int N = r.channels.size(); 732 for (int i = 0; i < N; i++) { 733 final NotificationChannel nc = r.channels.valueAt(i); 734 if (groupId.equals(nc.getGroup())) { 735 nc.setDeleted(true); 736 deletedChannels.add(nc); 737 } 738 } 739 updateConfig(); 740 return deletedChannels; 741 } 742 743 @Override 744 public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 745 int uid) { 746 Record r = getRecord(pkg, uid); 747 if (r == null) { 748 return new ArrayList<>(); 749 } 750 return r.groups.values(); 751 } 752 753 @Override 754 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, 755 boolean includeDeleted) { 756 Preconditions.checkNotNull(pkg); 757 List<NotificationChannel> channels = new ArrayList<>(); 758 Record r = getRecord(pkg, uid); 759 if (r == null) { 760 return ParceledListSlice.emptyList(); 761 } 762 int N = r.channels.size(); 763 for (int i = 0; i < N; i++) { 764 final NotificationChannel nc = r.channels.valueAt(i); 765 if (includeDeleted || !nc.isDeleted()) { 766 channels.add(nc); 767 } 768 } 769 return new ParceledListSlice<>(channels); 770 } 771 772 public int getDeletedChannelCount(String pkg, int uid) { 773 Preconditions.checkNotNull(pkg); 774 int deletedCount = 0; 775 Record r = getRecord(pkg, uid); 776 if (r == null) { 777 return deletedCount; 778 } 779 int N = r.channels.size(); 780 for (int i = 0; i < N; i++) { 781 final NotificationChannel nc = r.channels.valueAt(i); 782 if (nc.isDeleted()) { 783 deletedCount++; 784 } 785 } 786 return deletedCount; 787 } 788 789 /** 790 * Sets importance. 791 */ 792 @Override 793 public void setImportance(String pkgName, int uid, int importance) { 794 getOrCreateRecord(pkgName, uid).importance = importance; 795 updateConfig(); 796 } 797 798 public void setEnabled(String packageName, int uid, boolean enabled) { 799 boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE; 800 if (wasEnabled == enabled) { 801 return; 802 } 803 setImportance(packageName, uid, 804 enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE); 805 } 806 807 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 808 if (filter == null) { 809 final int N = mSignalExtractors.length; 810 pw.print(prefix); 811 pw.print("mSignalExtractors.length = "); 812 pw.println(N); 813 for (int i = 0; i < N; i++) { 814 pw.print(prefix); 815 pw.print(" "); 816 pw.println(mSignalExtractors[i]); 817 } 818 } 819 if (filter == null) { 820 pw.print(prefix); 821 pw.println("per-package config:"); 822 } 823 pw.println("Records:"); 824 synchronized (mRecords) { 825 dumpRecords(pw, prefix, filter, mRecords); 826 } 827 pw.println("Restored without uid:"); 828 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 829 } 830 831 private static void dumpRecords(PrintWriter pw, String prefix, 832 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 833 final int N = records.size(); 834 for (int i = 0; i < N; i++) { 835 final Record r = records.valueAt(i); 836 if (filter == null || filter.matches(r.pkg)) { 837 pw.print(prefix); 838 pw.print(" AppSettings: "); 839 pw.print(r.pkg); 840 pw.print(" ("); 841 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 842 pw.print(')'); 843 if (r.importance != DEFAULT_IMPORTANCE) { 844 pw.print(" importance="); 845 pw.print(Ranking.importanceToString(r.importance)); 846 } 847 if (r.priority != DEFAULT_PRIORITY) { 848 pw.print(" priority="); 849 pw.print(Notification.priorityToString(r.priority)); 850 } 851 if (r.visibility != DEFAULT_VISIBILITY) { 852 pw.print(" visibility="); 853 pw.print(Notification.visibilityToString(r.visibility)); 854 } 855 pw.print(" showBadge="); 856 pw.print(Boolean.toString(r.showBadge)); 857 pw.println(); 858 for (NotificationChannel channel : r.channels.values()) { 859 pw.print(prefix); 860 pw.print(" "); 861 pw.print(" "); 862 pw.println(channel); 863 } 864 for (NotificationChannelGroup group : r.groups.values()) { 865 pw.print(prefix); 866 pw.print(" "); 867 pw.print(" "); 868 pw.println(group); 869 } 870 } 871 } 872 } 873 874 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 875 JSONObject ranking = new JSONObject(); 876 JSONArray records = new JSONArray(); 877 try { 878 ranking.put("noUid", mRestoredWithoutUids.size()); 879 } catch (JSONException e) { 880 // pass 881 } 882 synchronized (mRecords) { 883 final int N = mRecords.size(); 884 for (int i = 0; i < N; i++) { 885 final Record r = mRecords.valueAt(i); 886 if (filter == null || filter.matches(r.pkg)) { 887 JSONObject record = new JSONObject(); 888 try { 889 record.put("userId", UserHandle.getUserId(r.uid)); 890 record.put("packageName", r.pkg); 891 if (r.importance != DEFAULT_IMPORTANCE) { 892 record.put("importance", Ranking.importanceToString(r.importance)); 893 } 894 if (r.priority != DEFAULT_PRIORITY) { 895 record.put("priority", Notification.priorityToString(r.priority)); 896 } 897 if (r.visibility != DEFAULT_VISIBILITY) { 898 record.put("visibility", Notification.visibilityToString(r.visibility)); 899 } 900 if (r.showBadge != DEFAULT_SHOW_BADGE) { 901 record.put("showBadge", Boolean.valueOf(r.showBadge)); 902 } 903 for (NotificationChannel channel : r.channels.values()) { 904 record.put("channel", channel.toJson()); 905 } 906 for (NotificationChannelGroup group : r.groups.values()) { 907 record.put("group", group.toJson()); 908 } 909 } catch (JSONException e) { 910 // pass 911 } 912 records.put(record); 913 } 914 } 915 } 916 try { 917 ranking.put("records", records); 918 } catch (JSONException e) { 919 // pass 920 } 921 return ranking; 922 } 923 924 /** 925 * Dump only the ban information as structured JSON for the stats collector. 926 * 927 * This is intentionally redundant with {#link dumpJson} because the old 928 * scraper will expect this format. 929 * 930 * @param filter 931 * @return 932 */ 933 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 934 JSONArray bans = new JSONArray(); 935 Map<Integer, String> packageBans = getPackageBans(); 936 for(Entry<Integer, String> ban : packageBans.entrySet()) { 937 final int userId = UserHandle.getUserId(ban.getKey()); 938 final String packageName = ban.getValue(); 939 if (filter == null || filter.matches(packageName)) { 940 JSONObject banJson = new JSONObject(); 941 try { 942 banJson.put("userId", userId); 943 banJson.put("packageName", packageName); 944 } catch (JSONException e) { 945 e.printStackTrace(); 946 } 947 bans.put(banJson); 948 } 949 } 950 return bans; 951 } 952 953 public Map<Integer, String> getPackageBans() { 954 synchronized (mRecords) { 955 final int N = mRecords.size(); 956 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 957 for (int i = 0; i < N; i++) { 958 final Record r = mRecords.valueAt(i); 959 if (r.importance == NotificationManager.IMPORTANCE_NONE) { 960 packageBans.put(r.uid, r.pkg); 961 } 962 } 963 964 return packageBans; 965 } 966 } 967 968 /** 969 * Dump only the channel information as structured JSON for the stats collector. 970 * 971 * This is intentionally redundant with {#link dumpJson} because the old 972 * scraper will expect this format. 973 * 974 * @param filter 975 * @return 976 */ 977 public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) { 978 JSONArray channels = new JSONArray(); 979 Map<String, Integer> packageChannels = getPackageChannels(); 980 for(Entry<String, Integer> channelCount : packageChannels.entrySet()) { 981 final String packageName = channelCount.getKey(); 982 if (filter == null || filter.matches(packageName)) { 983 JSONObject channelCountJson = new JSONObject(); 984 try { 985 channelCountJson.put("packageName", packageName); 986 channelCountJson.put("channelCount", channelCount.getValue()); 987 } catch (JSONException e) { 988 e.printStackTrace(); 989 } 990 channels.put(channelCountJson); 991 } 992 } 993 return channels; 994 } 995 996 private Map<String, Integer> getPackageChannels() { 997 ArrayMap<String, Integer> packageChannels = new ArrayMap<>(); 998 synchronized (mRecords) { 999 for (int i = 0; i < mRecords.size(); i++) { 1000 final Record r = mRecords.valueAt(i); 1001 int channelCount = 0; 1002 for (int j = 0; j < r.channels.size(); j++) { 1003 if (!r.channels.valueAt(j).isDeleted()) { 1004 channelCount++; 1005 } 1006 } 1007 packageChannels.put(r.pkg, channelCount); 1008 } 1009 } 1010 return packageChannels; 1011 } 1012 1013 public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, 1014 int[] uidList) { 1015 if (pkgList == null || pkgList.length == 0) { 1016 return; // nothing to do 1017 } 1018 boolean updated = false; 1019 if (removingPackage) { 1020 // Remove notification settings for uninstalled package 1021 int size = Math.min(pkgList.length, uidList.length); 1022 for (int i = 0; i < size; i++) { 1023 final String pkg = pkgList[i]; 1024 final int uid = uidList[i]; 1025 synchronized (mRecords) { 1026 mRecords.remove(recordKey(pkg, uid)); 1027 } 1028 mRestoredWithoutUids.remove(pkg); 1029 updated = true; 1030 } 1031 } else { 1032 for (String pkg : pkgList) { 1033 // Package install 1034 final Record r = mRestoredWithoutUids.get(pkg); 1035 if (r != null) { 1036 try { 1037 r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); 1038 mRestoredWithoutUids.remove(pkg); 1039 synchronized (mRecords) { 1040 mRecords.put(recordKey(r.pkg, r.uid), r); 1041 } 1042 updated = true; 1043 } catch (NameNotFoundException e) { 1044 // noop 1045 } 1046 } 1047 // Package upgrade 1048 try { 1049 Record fullRecord = getRecord(pkg, 1050 mPm.getPackageUidAsUser(pkg, changeUserId)); 1051 if (fullRecord != null) { 1052 deleteDefaultChannelIfNeeded(fullRecord); 1053 } 1054 } catch (NameNotFoundException e) {} 1055 } 1056 } 1057 1058 if (updated) { 1059 updateConfig(); 1060 } 1061 } 1062 1063 private LogMaker getChannelLog(NotificationChannel channel, String pkg) { 1064 return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL) 1065 .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) 1066 .setPackageName(pkg) 1067 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, 1068 channel.getId()) 1069 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, 1070 channel.getImportance()); 1071 } 1072 1073 private static class Record { 1074 static int UNKNOWN_UID = UserHandle.USER_NULL; 1075 1076 String pkg; 1077 int uid = UNKNOWN_UID; 1078 int importance = DEFAULT_IMPORTANCE; 1079 int priority = DEFAULT_PRIORITY; 1080 int visibility = DEFAULT_VISIBILITY; 1081 boolean showBadge = DEFAULT_SHOW_BADGE; 1082 1083 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 1084 ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>(); 1085 } 1086} 1087