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