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