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