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