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