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