RankingHelper.java revision df44b606f357bb67e7a3b44e58f551c1c731ce42
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 public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, 674 int uid) { 675 Preconditions.checkNotNull(pkg); 676 Record r = getRecord(pkg, uid); 677 return r.groups.get(groupId); 678 } 679 680 @Override 681 public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 682 int uid, boolean includeDeleted) { 683 Preconditions.checkNotNull(pkg); 684 Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); 685 Record r = getRecord(pkg, uid); 686 if (r == null) { 687 return ParceledListSlice.emptyList(); 688 } 689 NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); 690 int N = r.channels.size(); 691 for (int i = 0; i < N; i++) { 692 final NotificationChannel nc = r.channels.valueAt(i); 693 if (includeDeleted || !nc.isDeleted()) { 694 if (nc.getGroup() != null) { 695 NotificationChannelGroup ncg = groups.get(nc.getGroup()); 696 if (ncg == null ) { 697 ncg = r.groups.get(nc.getGroup()).clone(); 698 groups.put(nc.getGroup(), ncg); 699 } 700 ncg.addChannel(nc); 701 } else { 702 nonGrouped.addChannel(nc); 703 } 704 } 705 } 706 if (nonGrouped.getChannels().size() > 0) { 707 groups.put(null, nonGrouped); 708 } 709 return new ParceledListSlice<>(new ArrayList<>(groups.values())); 710 } 711 712 @Override 713 @VisibleForTesting 714 public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 715 int uid) { 716 Record r = getRecord(pkg, uid); 717 if (r == null) { 718 return new ArrayList<>(); 719 } 720 return r.groups.values(); 721 } 722 723 @Override 724 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, 725 boolean includeDeleted) { 726 Preconditions.checkNotNull(pkg); 727 List<NotificationChannel> channels = new ArrayList<>(); 728 Record r = getRecord(pkg, uid); 729 if (r == null) { 730 return ParceledListSlice.emptyList(); 731 } 732 int N = r.channels.size(); 733 for (int i = 0; i < N; i++) { 734 final NotificationChannel nc = r.channels.valueAt(i); 735 if (includeDeleted || !nc.isDeleted()) { 736 channels.add(nc); 737 } 738 } 739 return new ParceledListSlice<>(channels); 740 } 741 742 /** 743 * Sets importance. 744 */ 745 @Override 746 public void setImportance(String pkgName, int uid, int importance) { 747 getOrCreateRecord(pkgName, uid).importance = importance; 748 updateConfig(); 749 } 750 751 public void setEnabled(String packageName, int uid, boolean enabled) { 752 boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE; 753 if (wasEnabled == enabled) { 754 return; 755 } 756 setImportance(packageName, uid, 757 enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE); 758 } 759 760 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 761 if (filter == null) { 762 final int N = mSignalExtractors.length; 763 pw.print(prefix); 764 pw.print("mSignalExtractors.length = "); 765 pw.println(N); 766 for (int i = 0; i < N; i++) { 767 pw.print(prefix); 768 pw.print(" "); 769 pw.println(mSignalExtractors[i]); 770 } 771 } 772 if (filter == null) { 773 pw.print(prefix); 774 pw.println("per-package config:"); 775 } 776 pw.println("Records:"); 777 dumpRecords(pw, prefix, filter, mRecords); 778 pw.println("Restored without uid:"); 779 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 780 } 781 782 private static void dumpRecords(PrintWriter pw, String prefix, 783 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 784 final int N = records.size(); 785 for (int i = 0; i < N; i++) { 786 final Record r = records.valueAt(i); 787 if (filter == null || filter.matches(r.pkg)) { 788 pw.print(prefix); 789 pw.print(" AppSettings: "); 790 pw.print(r.pkg); 791 pw.print(" ("); 792 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 793 pw.print(')'); 794 if (r.importance != DEFAULT_IMPORTANCE) { 795 pw.print(" importance="); 796 pw.print(Ranking.importanceToString(r.importance)); 797 } 798 if (r.priority != DEFAULT_PRIORITY) { 799 pw.print(" priority="); 800 pw.print(Notification.priorityToString(r.priority)); 801 } 802 if (r.visibility != DEFAULT_VISIBILITY) { 803 pw.print(" visibility="); 804 pw.print(Notification.visibilityToString(r.visibility)); 805 } 806 pw.print(" showBadge="); 807 pw.print(Boolean.toString(r.showBadge)); 808 pw.println(); 809 for (NotificationChannel channel : r.channels.values()) { 810 pw.print(prefix); 811 pw.print(" "); 812 pw.print(" "); 813 pw.println(channel); 814 } 815 for (NotificationChannelGroup group : r.groups.values()) { 816 pw.print(prefix); 817 pw.print(" "); 818 pw.print(" "); 819 pw.println(group); 820 } 821 } 822 } 823 } 824 825 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 826 JSONObject ranking = new JSONObject(); 827 JSONArray records = new JSONArray(); 828 try { 829 ranking.put("noUid", mRestoredWithoutUids.size()); 830 } catch (JSONException e) { 831 // pass 832 } 833 final int N = mRecords.size(); 834 for (int i = 0; i < N; i++) { 835 final Record r = mRecords.valueAt(i); 836 if (filter == null || filter.matches(r.pkg)) { 837 JSONObject record = new JSONObject(); 838 try { 839 record.put("userId", UserHandle.getUserId(r.uid)); 840 record.put("packageName", r.pkg); 841 if (r.importance != DEFAULT_IMPORTANCE) { 842 record.put("importance", Ranking.importanceToString(r.importance)); 843 } 844 if (r.priority != DEFAULT_PRIORITY) { 845 record.put("priority", Notification.priorityToString(r.priority)); 846 } 847 if (r.visibility != DEFAULT_VISIBILITY) { 848 record.put("visibility", Notification.visibilityToString(r.visibility)); 849 } 850 if (r.showBadge != DEFAULT_SHOW_BADGE) { 851 record.put("showBadge", Boolean.valueOf(r.showBadge)); 852 } 853 for (NotificationChannel channel : r.channels.values()) { 854 record.put("channel", channel.toJson()); 855 } 856 for (NotificationChannelGroup group : r.groups.values()) { 857 record.put("group", group.toJson()); 858 } 859 } catch (JSONException e) { 860 // pass 861 } 862 records.put(record); 863 } 864 } 865 try { 866 ranking.put("records", records); 867 } catch (JSONException e) { 868 // pass 869 } 870 return ranking; 871 } 872 873 /** 874 * Dump only the ban information as structured JSON for the stats collector. 875 * 876 * This is intentionally redundant with {#link dumpJson} because the old 877 * scraper will expect this format. 878 * 879 * @param filter 880 * @return 881 */ 882 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 883 JSONArray bans = new JSONArray(); 884 Map<Integer, String> packageBans = getPackageBans(); 885 for(Entry<Integer, String> ban : packageBans.entrySet()) { 886 final int userId = UserHandle.getUserId(ban.getKey()); 887 final String packageName = ban.getValue(); 888 if (filter == null || filter.matches(packageName)) { 889 JSONObject banJson = new JSONObject(); 890 try { 891 banJson.put("userId", userId); 892 banJson.put("packageName", packageName); 893 } catch (JSONException e) { 894 e.printStackTrace(); 895 } 896 bans.put(banJson); 897 } 898 } 899 return bans; 900 } 901 902 public Map<Integer, String> getPackageBans() { 903 final int N = mRecords.size(); 904 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 905 for (int i = 0; i < N; i++) { 906 final Record r = mRecords.valueAt(i); 907 if (r.importance == NotificationManager.IMPORTANCE_NONE) { 908 packageBans.put(r.uid, r.pkg); 909 } 910 } 911 return packageBans; 912 } 913 914 public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, 915 int[] uidList) { 916 if (pkgList == null || pkgList.length == 0) { 917 return; // nothing to do 918 } 919 boolean updated = false; 920 if (removingPackage) { 921 // Remove notification settings for uninstalled package 922 int size = Math.min(pkgList.length, uidList.length); 923 for (int i = 0; i < size; i++) { 924 final String pkg = pkgList[i]; 925 final int uid = uidList[i]; 926 mRecords.remove(recordKey(pkg, uid)); 927 mRestoredWithoutUids.remove(pkg); 928 updated = true; 929 } 930 } else { 931 for (String pkg : pkgList) { 932 // Package install 933 final Record r = mRestoredWithoutUids.get(pkg); 934 if (r != null) { 935 try { 936 r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); 937 mRestoredWithoutUids.remove(pkg); 938 mRecords.put(recordKey(r.pkg, r.uid), r); 939 updated = true; 940 } catch (NameNotFoundException e) { 941 // noop 942 } 943 } 944 // Package upgrade 945 try { 946 Record fullRecord = getRecord(pkg, 947 mPm.getPackageUidAsUser(pkg, changeUserId)); 948 if (fullRecord != null) { 949 clampDefaultChannel(fullRecord); 950 } 951 } catch (NameNotFoundException e) { 952 } 953 } 954 } 955 956 if (updated) { 957 updateConfig(); 958 } 959 } 960 961 private static class Record { 962 static int UNKNOWN_UID = UserHandle.USER_NULL; 963 964 String pkg; 965 int uid = UNKNOWN_UID; 966 int importance = DEFAULT_IMPORTANCE; 967 int priority = DEFAULT_PRIORITY; 968 int visibility = DEFAULT_VISIBILITY; 969 boolean showBadge = DEFAULT_SHOW_BADGE; 970 971 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 972 ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>(); 973 } 974} 975