RankingHelper.java revision 74856c4a611b337ded6d3eee14c710cf5f7aae77
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.enableLights(updatedChannel.shouldShowLights()); 567 channel.setLightColor(updatedChannel.getLightColor()); 568 } 569 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0) { 570 channel.setBypassDnd(updatedChannel.canBypassDnd()); 571 } 572 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) { 573 channel.setSound(updatedChannel.getSound(), updatedChannel.getAudioAttributes()); 574 } 575 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) { 576 channel.enableVibration(updatedChannel.shouldVibrate()); 577 channel.setVibrationPattern(updatedChannel.getVibrationPattern()); 578 } 579 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) { 580 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 581 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 582 } else { 583 channel.setLockscreenVisibility(updatedChannel.getLockscreenVisibility()); 584 } 585 } 586 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0) { 587 channel.setShowBadge(updatedChannel.canShowBadge()); 588 } 589 if (updatedChannel.isDeleted()) { 590 channel.setDeleted(true); 591 } 592 // Assistant cannot change the group 593 594 r.channels.put(channel.getId(), channel); 595 updateConfig(); 596 } 597 598 @Override 599 public NotificationChannel getNotificationChannelWithFallback(String pkg, int uid, 600 String channelId, boolean includeDeleted) { 601 Record r = getOrCreateRecord(pkg, uid); 602 if (channelId == null) { 603 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 604 } 605 NotificationChannel channel = r.channels.get(channelId); 606 if (channel != null && (includeDeleted || !channel.isDeleted())) { 607 return channel; 608 } else { 609 return r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID); 610 } 611 } 612 613 @Override 614 public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, 615 boolean includeDeleted) { 616 Preconditions.checkNotNull(pkg); 617 Record r = getOrCreateRecord(pkg, uid); 618 if (r == null) { 619 return null; 620 } 621 if (channelId == null) { 622 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 623 } 624 final NotificationChannel nc = r.channels.get(channelId); 625 if (nc != null && (includeDeleted || !nc.isDeleted())) { 626 return nc; 627 } 628 return null; 629 } 630 631 @Override 632 public void deleteNotificationChannel(String pkg, int uid, String channelId) { 633 Preconditions.checkNotNull(pkg); 634 Preconditions.checkNotNull(channelId); 635 Record r = getRecord(pkg, uid); 636 if (r == null) { 637 return; 638 } 639 NotificationChannel channel = r.channels.get(channelId); 640 if (channel != null) { 641 channel.setDeleted(true); 642 } 643 } 644 645 @Override 646 @VisibleForTesting 647 public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { 648 Preconditions.checkNotNull(pkg); 649 Preconditions.checkNotNull(channelId); 650 Record r = getRecord(pkg, uid); 651 if (r == null) { 652 return; 653 } 654 r.channels.remove(channelId); 655 } 656 657 @Override 658 public void permanentlyDeleteNotificationChannels(String pkg, int uid) { 659 Preconditions.checkNotNull(pkg); 660 Record r = getRecord(pkg, uid); 661 if (r == null) { 662 return; 663 } 664 int N = r.channels.size() - 1; 665 for (int i = N; i >= 0; i--) { 666 String key = r.channels.keyAt(i); 667 if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { 668 r.channels.remove(key); 669 } 670 } 671 } 672 673 @Override 674 public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 675 int uid, boolean includeDeleted) { 676 Preconditions.checkNotNull(pkg); 677 Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); 678 Record r = getRecord(pkg, uid); 679 if (r == null) { 680 return ParceledListSlice.emptyList(); 681 } 682 NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); 683 int N = r.channels.size(); 684 for (int i = 0; i < N; i++) { 685 final NotificationChannel nc = r.channels.valueAt(i); 686 if (includeDeleted || !nc.isDeleted()) { 687 if (nc.getGroup() != null) { 688 NotificationChannelGroup ncg = groups.get(nc.getGroup()); 689 if (ncg == null ) { 690 ncg = r.groups.get(nc.getGroup()).clone(); 691 groups.put(nc.getGroup(), ncg); 692 } 693 ncg.addChannel(nc); 694 } else { 695 nonGrouped.addChannel(nc); 696 } 697 } 698 } 699 if (nonGrouped.getChannels().size() > 0) { 700 groups.put(null, nonGrouped); 701 } 702 return new ParceledListSlice<>(new ArrayList<>(groups.values())); 703 } 704 705 @Override 706 @VisibleForTesting 707 public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 708 int uid) { 709 Record r = getRecord(pkg, uid); 710 if (r == null) { 711 return new ArrayList<>(); 712 } 713 return r.groups.values(); 714 } 715 716 @Override 717 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, 718 boolean includeDeleted) { 719 Preconditions.checkNotNull(pkg); 720 List<NotificationChannel> channels = new ArrayList<>(); 721 Record r = getRecord(pkg, uid); 722 if (r == null) { 723 return ParceledListSlice.emptyList(); 724 } 725 int N = r.channels.size(); 726 for (int i = 0; i < N; i++) { 727 final NotificationChannel nc = r.channels.valueAt(i); 728 if (includeDeleted || !nc.isDeleted()) { 729 channels.add(nc); 730 } 731 } 732 return new ParceledListSlice<>(channels); 733 } 734 735 /** 736 * Sets importance. 737 */ 738 @Override 739 public void setImportance(String pkgName, int uid, int importance) { 740 getOrCreateRecord(pkgName, uid).importance = importance; 741 updateConfig(); 742 } 743 744 public void setEnabled(String packageName, int uid, boolean enabled) { 745 boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE; 746 if (wasEnabled == enabled) { 747 return; 748 } 749 setImportance(packageName, uid, 750 enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE); 751 } 752 753 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 754 if (filter == null) { 755 final int N = mSignalExtractors.length; 756 pw.print(prefix); 757 pw.print("mSignalExtractors.length = "); 758 pw.println(N); 759 for (int i = 0; i < N; i++) { 760 pw.print(prefix); 761 pw.print(" "); 762 pw.println(mSignalExtractors[i]); 763 } 764 } 765 if (filter == null) { 766 pw.print(prefix); 767 pw.println("per-package config:"); 768 } 769 pw.println("Records:"); 770 dumpRecords(pw, prefix, filter, mRecords); 771 pw.println("Restored without uid:"); 772 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 773 } 774 775 private static void dumpRecords(PrintWriter pw, String prefix, 776 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 777 final int N = records.size(); 778 for (int i = 0; i < N; i++) { 779 final Record r = records.valueAt(i); 780 if (filter == null || filter.matches(r.pkg)) { 781 pw.print(prefix); 782 pw.print(" AppSettings: "); 783 pw.print(r.pkg); 784 pw.print(" ("); 785 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 786 pw.print(')'); 787 if (r.importance != DEFAULT_IMPORTANCE) { 788 pw.print(" importance="); 789 pw.print(Ranking.importanceToString(r.importance)); 790 } 791 if (r.priority != DEFAULT_PRIORITY) { 792 pw.print(" priority="); 793 pw.print(Notification.priorityToString(r.priority)); 794 } 795 if (r.visibility != DEFAULT_VISIBILITY) { 796 pw.print(" visibility="); 797 pw.print(Notification.visibilityToString(r.visibility)); 798 } 799 pw.print(" showBadge="); 800 pw.print(Boolean.toString(r.showBadge)); 801 pw.println(); 802 for (NotificationChannel channel : r.channels.values()) { 803 pw.print(prefix); 804 pw.print(" "); 805 pw.print(" "); 806 pw.println(channel); 807 } 808 for (NotificationChannelGroup group : r.groups.values()) { 809 pw.print(prefix); 810 pw.print(" "); 811 pw.print(" "); 812 pw.println(group); 813 } 814 } 815 } 816 } 817 818 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 819 JSONObject ranking = new JSONObject(); 820 JSONArray records = new JSONArray(); 821 try { 822 ranking.put("noUid", mRestoredWithoutUids.size()); 823 } catch (JSONException e) { 824 // pass 825 } 826 final int N = mRecords.size(); 827 for (int i = 0; i < N; i++) { 828 final Record r = mRecords.valueAt(i); 829 if (filter == null || filter.matches(r.pkg)) { 830 JSONObject record = new JSONObject(); 831 try { 832 record.put("userId", UserHandle.getUserId(r.uid)); 833 record.put("packageName", r.pkg); 834 if (r.importance != DEFAULT_IMPORTANCE) { 835 record.put("importance", Ranking.importanceToString(r.importance)); 836 } 837 if (r.priority != DEFAULT_PRIORITY) { 838 record.put("priority", Notification.priorityToString(r.priority)); 839 } 840 if (r.visibility != DEFAULT_VISIBILITY) { 841 record.put("visibility", Notification.visibilityToString(r.visibility)); 842 } 843 if (r.showBadge != DEFAULT_SHOW_BADGE) { 844 record.put("showBadge", Boolean.valueOf(r.showBadge)); 845 } 846 for (NotificationChannel channel : r.channels.values()) { 847 record.put("channel", channel.toJson()); 848 } 849 for (NotificationChannelGroup group : r.groups.values()) { 850 record.put("group", group.toJson()); 851 } 852 } catch (JSONException e) { 853 // pass 854 } 855 records.put(record); 856 } 857 } 858 try { 859 ranking.put("records", records); 860 } catch (JSONException e) { 861 // pass 862 } 863 return ranking; 864 } 865 866 /** 867 * Dump only the ban information as structured JSON for the stats collector. 868 * 869 * This is intentionally redundant with {#link dumpJson} because the old 870 * scraper will expect this format. 871 * 872 * @param filter 873 * @return 874 */ 875 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 876 JSONArray bans = new JSONArray(); 877 Map<Integer, String> packageBans = getPackageBans(); 878 for(Entry<Integer, String> ban : packageBans.entrySet()) { 879 final int userId = UserHandle.getUserId(ban.getKey()); 880 final String packageName = ban.getValue(); 881 if (filter == null || filter.matches(packageName)) { 882 JSONObject banJson = new JSONObject(); 883 try { 884 banJson.put("userId", userId); 885 banJson.put("packageName", packageName); 886 } catch (JSONException e) { 887 e.printStackTrace(); 888 } 889 bans.put(banJson); 890 } 891 } 892 return bans; 893 } 894 895 public Map<Integer, String> getPackageBans() { 896 final int N = mRecords.size(); 897 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 898 for (int i = 0; i < N; i++) { 899 final Record r = mRecords.valueAt(i); 900 if (r.importance == NotificationManager.IMPORTANCE_NONE) { 901 packageBans.put(r.uid, r.pkg); 902 } 903 } 904 return packageBans; 905 } 906 907 public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, 908 int[] uidList) { 909 if (pkgList == null || pkgList.length == 0) { 910 return; // nothing to do 911 } 912 boolean updated = false; 913 if (removingPackage) { 914 // Remove notification settings for uninstalled package 915 int size = Math.min(pkgList.length, uidList.length); 916 for (int i = 0; i < size; i++) { 917 final String pkg = pkgList[i]; 918 final int uid = uidList[i]; 919 mRecords.remove(recordKey(pkg, uid)); 920 mRestoredWithoutUids.remove(pkg); 921 updated = true; 922 } 923 } else { 924 for (String pkg : pkgList) { 925 // Package install 926 final Record r = mRestoredWithoutUids.get(pkg); 927 if (r != null) { 928 try { 929 r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); 930 mRestoredWithoutUids.remove(pkg); 931 mRecords.put(recordKey(r.pkg, r.uid), r); 932 updated = true; 933 } catch (NameNotFoundException e) { 934 // noop 935 } 936 } 937 // Package upgrade 938 try { 939 Record fullRecord = getRecord(pkg, 940 mPm.getPackageUidAsUser(pkg, changeUserId)); 941 if (fullRecord != null) { 942 clampDefaultChannel(fullRecord); 943 } 944 } catch (NameNotFoundException e) { 945 } 946 } 947 } 948 949 if (updated) { 950 updateConfig(); 951 } 952 } 953 954 private static class Record { 955 static int UNKNOWN_UID = UserHandle.USER_NULL; 956 957 String pkg; 958 int uid = UNKNOWN_UID; 959 int importance = DEFAULT_IMPORTANCE; 960 int priority = DEFAULT_PRIORITY; 961 int visibility = DEFAULT_VISIBILITY; 962 boolean showBadge = DEFAULT_SHOW_BADGE; 963 964 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 965 ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>(); 966 } 967} 968