RankingHelper.java revision 3d91f1109b3921b6e67d28463a9fb1bc6f537791
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, except deleted status and name 525 if (existing != null && fromTargetApp) { 526 if (existing.isDeleted()) { 527 existing.setDeleted(false); 528 } 529 530 existing.setNameResId(channel.getNameResId()); 531 532 MetricsLogger.action(getChannelLog(channel, pkg)); 533 updateConfig(); 534 return; 535 } 536 if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE 537 || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) { 538 throw new IllegalArgumentException("Invalid importance level"); 539 } 540 // Reset fields that apps aren't allowed to set. 541 if (fromTargetApp) { 542 channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX); 543 channel.setLockscreenVisibility(r.visibility); 544 } 545 clearLockedFields(channel); 546 if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 547 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 548 } 549 if (!r.showBadge) { 550 channel.setShowBadge(false); 551 } 552 if (channel.getSound() == null) { 553 channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, 554 Notification.AUDIO_ATTRIBUTES_DEFAULT); 555 } 556 r.channels.put(channel.getId(), channel); 557 MetricsLogger.action(getChannelLog(channel, pkg).setType( 558 MetricsProto.MetricsEvent.TYPE_OPEN)); 559 updateConfig(); 560 } 561 562 private void clearLockedFields(NotificationChannel channel) { 563 int clearMask = 0; 564 for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) { 565 clearMask |= NotificationChannel.LOCKABLE_FIELDS[i]; 566 } 567 channel.lockFields(~clearMask); 568 } 569 570 @Override 571 public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) { 572 Preconditions.checkNotNull(updatedChannel); 573 Preconditions.checkNotNull(updatedChannel.getId()); 574 Record r = getOrCreateRecord(pkg, uid); 575 if (r == null) { 576 throw new IllegalArgumentException("Invalid package"); 577 } 578 NotificationChannel channel = r.channels.get(updatedChannel.getId()); 579 if (channel == null || channel.isDeleted()) { 580 throw new IllegalArgumentException("Channel does not exist"); 581 } 582 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 583 updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 584 } 585 r.channels.put(updatedChannel.getId(), updatedChannel); 586 587 MetricsLogger.action(getChannelLog(updatedChannel, pkg)); 588 updateConfig(); 589 } 590 591 @Override 592 public void updateNotificationChannelFromAssistant(String pkg, int uid, 593 NotificationChannel updatedChannel) { 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 603 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) { 604 channel.setImportance(updatedChannel.getImportance()); 605 } 606 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_LIGHTS) == 0) { 607 channel.enableLights(updatedChannel.shouldShowLights()); 608 channel.setLightColor(updatedChannel.getLightColor()); 609 } 610 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0) { 611 channel.setBypassDnd(updatedChannel.canBypassDnd()); 612 } 613 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) { 614 channel.setSound(updatedChannel.getSound(), updatedChannel.getAudioAttributes()); 615 } 616 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) { 617 channel.enableVibration(updatedChannel.shouldVibrate()); 618 channel.setVibrationPattern(updatedChannel.getVibrationPattern()); 619 } 620 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) { 621 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 622 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 623 } else { 624 channel.setLockscreenVisibility(updatedChannel.getLockscreenVisibility()); 625 } 626 } 627 if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0) { 628 channel.setShowBadge(updatedChannel.canShowBadge()); 629 } 630 if (updatedChannel.isDeleted()) { 631 channel.setDeleted(true); 632 } 633 // Assistant cannot change the group 634 635 MetricsLogger.action(getChannelLog(channel, pkg)); 636 r.channels.put(channel.getId(), channel); 637 updateConfig(); 638 } 639 640 @Override 641 public NotificationChannel getNotificationChannelWithFallback(String pkg, int uid, 642 String channelId, boolean includeDeleted) { 643 Record r = getOrCreateRecord(pkg, uid); 644 if (channelId == null) { 645 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 646 } 647 NotificationChannel channel = r.channels.get(channelId); 648 if (channel != null && (includeDeleted || !channel.isDeleted())) { 649 return channel; 650 } else { 651 return r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID); 652 } 653 } 654 655 @Override 656 public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId, 657 boolean includeDeleted) { 658 Preconditions.checkNotNull(pkg); 659 Record r = getOrCreateRecord(pkg, uid); 660 if (r == null) { 661 return null; 662 } 663 if (channelId == null) { 664 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 665 } 666 final NotificationChannel nc = r.channels.get(channelId); 667 if (nc != null && (includeDeleted || !nc.isDeleted())) { 668 return nc; 669 } 670 return null; 671 } 672 673 @Override 674 public void deleteNotificationChannel(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 NotificationChannel channel = r.channels.get(channelId); 682 if (channel != null) { 683 channel.setDeleted(true); 684 } 685 LogMaker lm = getChannelLog(channel, pkg); 686 lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE); 687 MetricsLogger.action(lm); 688 } 689 690 @Override 691 @VisibleForTesting 692 public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) { 693 Preconditions.checkNotNull(pkg); 694 Preconditions.checkNotNull(channelId); 695 Record r = getRecord(pkg, uid); 696 if (r == null) { 697 return; 698 } 699 r.channels.remove(channelId); 700 } 701 702 @Override 703 public void permanentlyDeleteNotificationChannels(String pkg, int uid) { 704 Preconditions.checkNotNull(pkg); 705 Record r = getRecord(pkg, uid); 706 if (r == null) { 707 return; 708 } 709 int N = r.channels.size() - 1; 710 for (int i = N; i >= 0; i--) { 711 String key = r.channels.keyAt(i); 712 if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) { 713 r.channels.remove(key); 714 } 715 } 716 } 717 718 public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg, 719 int uid) { 720 Preconditions.checkNotNull(pkg); 721 Record r = getRecord(pkg, uid); 722 return r.groups.get(groupId); 723 } 724 725 @Override 726 public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 727 int uid, boolean includeDeleted) { 728 Preconditions.checkNotNull(pkg); 729 Map<String, NotificationChannelGroup> groups = new ArrayMap<>(); 730 Record r = getRecord(pkg, uid); 731 if (r == null) { 732 return ParceledListSlice.emptyList(); 733 } 734 NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null); 735 int N = r.channels.size(); 736 for (int i = 0; i < N; i++) { 737 final NotificationChannel nc = r.channels.valueAt(i); 738 if (includeDeleted || !nc.isDeleted()) { 739 if (nc.getGroup() != null) { 740 NotificationChannelGroup ncg = groups.get(nc.getGroup()); 741 if (ncg == null ) { 742 ncg = r.groups.get(nc.getGroup()).clone(); 743 groups.put(nc.getGroup(), ncg); 744 } 745 ncg.addChannel(nc); 746 } else { 747 nonGrouped.addChannel(nc); 748 } 749 } 750 } 751 if (nonGrouped.getChannels().size() > 0) { 752 groups.put(null, nonGrouped); 753 } 754 return new ParceledListSlice<>(new ArrayList<>(groups.values())); 755 } 756 757 @Override 758 @VisibleForTesting 759 public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg, 760 int uid) { 761 Record r = getRecord(pkg, uid); 762 if (r == null) { 763 return new ArrayList<>(); 764 } 765 return r.groups.values(); 766 } 767 768 @Override 769 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid, 770 boolean includeDeleted) { 771 Preconditions.checkNotNull(pkg); 772 List<NotificationChannel> channels = new ArrayList<>(); 773 Record r = getRecord(pkg, uid); 774 if (r == null) { 775 return ParceledListSlice.emptyList(); 776 } 777 int N = r.channels.size(); 778 for (int i = 0; i < N; i++) { 779 final NotificationChannel nc = r.channels.valueAt(i); 780 if (includeDeleted || !nc.isDeleted()) { 781 channels.add(nc); 782 } 783 } 784 return new ParceledListSlice<>(channels); 785 } 786 787 /** 788 * Sets importance. 789 */ 790 @Override 791 public void setImportance(String pkgName, int uid, int importance) { 792 getOrCreateRecord(pkgName, uid).importance = importance; 793 updateConfig(); 794 } 795 796 public void setEnabled(String packageName, int uid, boolean enabled) { 797 boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE; 798 if (wasEnabled == enabled) { 799 return; 800 } 801 setImportance(packageName, uid, 802 enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE); 803 } 804 805 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 806 if (filter == null) { 807 final int N = mSignalExtractors.length; 808 pw.print(prefix); 809 pw.print("mSignalExtractors.length = "); 810 pw.println(N); 811 for (int i = 0; i < N; i++) { 812 pw.print(prefix); 813 pw.print(" "); 814 pw.println(mSignalExtractors[i]); 815 } 816 } 817 if (filter == null) { 818 pw.print(prefix); 819 pw.println("per-package config:"); 820 } 821 pw.println("Records:"); 822 dumpRecords(pw, prefix, filter, mRecords); 823 pw.println("Restored without uid:"); 824 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 825 } 826 827 private static void dumpRecords(PrintWriter pw, String prefix, 828 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 829 final int N = records.size(); 830 for (int i = 0; i < N; i++) { 831 final Record r = records.valueAt(i); 832 if (filter == null || filter.matches(r.pkg)) { 833 pw.print(prefix); 834 pw.print(" AppSettings: "); 835 pw.print(r.pkg); 836 pw.print(" ("); 837 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 838 pw.print(')'); 839 if (r.importance != DEFAULT_IMPORTANCE) { 840 pw.print(" importance="); 841 pw.print(Ranking.importanceToString(r.importance)); 842 } 843 if (r.priority != DEFAULT_PRIORITY) { 844 pw.print(" priority="); 845 pw.print(Notification.priorityToString(r.priority)); 846 } 847 if (r.visibility != DEFAULT_VISIBILITY) { 848 pw.print(" visibility="); 849 pw.print(Notification.visibilityToString(r.visibility)); 850 } 851 pw.print(" showBadge="); 852 pw.print(Boolean.toString(r.showBadge)); 853 pw.println(); 854 for (NotificationChannel channel : r.channels.values()) { 855 pw.print(prefix); 856 pw.print(" "); 857 pw.print(" "); 858 pw.println(channel); 859 } 860 for (NotificationChannelGroup group : r.groups.values()) { 861 pw.print(prefix); 862 pw.print(" "); 863 pw.print(" "); 864 pw.println(group); 865 } 866 } 867 } 868 } 869 870 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 871 JSONObject ranking = new JSONObject(); 872 JSONArray records = new JSONArray(); 873 try { 874 ranking.put("noUid", mRestoredWithoutUids.size()); 875 } catch (JSONException e) { 876 // pass 877 } 878 final int N = mRecords.size(); 879 for (int i = 0; i < N; i++) { 880 final Record r = mRecords.valueAt(i); 881 if (filter == null || filter.matches(r.pkg)) { 882 JSONObject record = new JSONObject(); 883 try { 884 record.put("userId", UserHandle.getUserId(r.uid)); 885 record.put("packageName", r.pkg); 886 if (r.importance != DEFAULT_IMPORTANCE) { 887 record.put("importance", Ranking.importanceToString(r.importance)); 888 } 889 if (r.priority != DEFAULT_PRIORITY) { 890 record.put("priority", Notification.priorityToString(r.priority)); 891 } 892 if (r.visibility != DEFAULT_VISIBILITY) { 893 record.put("visibility", Notification.visibilityToString(r.visibility)); 894 } 895 if (r.showBadge != DEFAULT_SHOW_BADGE) { 896 record.put("showBadge", Boolean.valueOf(r.showBadge)); 897 } 898 for (NotificationChannel channel : r.channels.values()) { 899 record.put("channel", channel.toJson()); 900 } 901 for (NotificationChannelGroup group : r.groups.values()) { 902 record.put("group", group.toJson()); 903 } 904 } catch (JSONException e) { 905 // pass 906 } 907 records.put(record); 908 } 909 } 910 try { 911 ranking.put("records", records); 912 } catch (JSONException e) { 913 // pass 914 } 915 return ranking; 916 } 917 918 /** 919 * Dump only the ban information as structured JSON for the stats collector. 920 * 921 * This is intentionally redundant with {#link dumpJson} because the old 922 * scraper will expect this format. 923 * 924 * @param filter 925 * @return 926 */ 927 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 928 JSONArray bans = new JSONArray(); 929 Map<Integer, String> packageBans = getPackageBans(); 930 for(Entry<Integer, String> ban : packageBans.entrySet()) { 931 final int userId = UserHandle.getUserId(ban.getKey()); 932 final String packageName = ban.getValue(); 933 if (filter == null || filter.matches(packageName)) { 934 JSONObject banJson = new JSONObject(); 935 try { 936 banJson.put("userId", userId); 937 banJson.put("packageName", packageName); 938 } catch (JSONException e) { 939 e.printStackTrace(); 940 } 941 bans.put(banJson); 942 } 943 } 944 return bans; 945 } 946 947 public Map<Integer, String> getPackageBans() { 948 final int N = mRecords.size(); 949 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 950 for (int i = 0; i < N; i++) { 951 final Record r = mRecords.valueAt(i); 952 if (r.importance == NotificationManager.IMPORTANCE_NONE) { 953 packageBans.put(r.uid, r.pkg); 954 } 955 } 956 return packageBans; 957 } 958 959 /** 960 * Dump only the channel information as structured JSON for the stats collector. 961 * 962 * This is intentionally redundant with {#link dumpJson} because the old 963 * scraper will expect this format. 964 * 965 * @param filter 966 * @return 967 */ 968 public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) { 969 JSONArray channels = new JSONArray(); 970 Map<String, Integer> packageChannels = getPackageChannels(); 971 for(Entry<String, Integer> channelCount : packageChannels.entrySet()) { 972 final String packageName = channelCount.getKey(); 973 if (filter == null || filter.matches(packageName)) { 974 JSONObject channelCountJson = new JSONObject(); 975 try { 976 channelCountJson.put("packageName", packageName); 977 channelCountJson.put("channelCount", channelCount.getValue()); 978 } catch (JSONException e) { 979 e.printStackTrace(); 980 } 981 channels.put(channelCountJson); 982 } 983 } 984 return channels; 985 } 986 987 private Map<String, Integer> getPackageChannels() { 988 ArrayMap<String, Integer> packageChannels = new ArrayMap<>(); 989 for (int i = 0; i < mRecords.size(); i++) { 990 final Record r = mRecords.valueAt(i); 991 int channelCount = 0; 992 for (int j = 0; j < r.channels.size();j++) { 993 if (!r.channels.valueAt(j).isDeleted()) { 994 channelCount++; 995 } 996 } 997 packageChannels.put(r.pkg, channelCount); 998 } 999 return packageChannels; 1000 } 1001 1002 public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList, 1003 int[] uidList) { 1004 if (pkgList == null || pkgList.length == 0) { 1005 return; // nothing to do 1006 } 1007 boolean updated = false; 1008 if (removingPackage) { 1009 // Remove notification settings for uninstalled package 1010 int size = Math.min(pkgList.length, uidList.length); 1011 for (int i = 0; i < size; i++) { 1012 final String pkg = pkgList[i]; 1013 final int uid = uidList[i]; 1014 mRecords.remove(recordKey(pkg, uid)); 1015 mRestoredWithoutUids.remove(pkg); 1016 updated = true; 1017 } 1018 } else { 1019 for (String pkg : pkgList) { 1020 // Package install 1021 final Record r = mRestoredWithoutUids.get(pkg); 1022 if (r != null) { 1023 try { 1024 r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId); 1025 mRestoredWithoutUids.remove(pkg); 1026 mRecords.put(recordKey(r.pkg, r.uid), r); 1027 updated = true; 1028 } catch (NameNotFoundException e) { 1029 // noop 1030 } 1031 } 1032 // Package upgrade 1033 try { 1034 Record fullRecord = getRecord(pkg, 1035 mPm.getPackageUidAsUser(pkg, changeUserId)); 1036 if (fullRecord != null) { 1037 clampDefaultChannel(fullRecord); 1038 } 1039 } catch (NameNotFoundException e) { 1040 } 1041 } 1042 } 1043 1044 if (updated) { 1045 updateConfig(); 1046 } 1047 } 1048 1049 private LogMaker getChannelLog(NotificationChannel channel, String pkg) { 1050 return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL) 1051 .setType(MetricsProto.MetricsEvent.TYPE_UPDATE) 1052 .setPackageName(pkg) 1053 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID, 1054 channel.getId()) 1055 .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE, 1056 channel.getImportance()); 1057 } 1058 1059 private static class Record { 1060 static int UNKNOWN_UID = UserHandle.USER_NULL; 1061 1062 String pkg; 1063 int uid = UNKNOWN_UID; 1064 int importance = DEFAULT_IMPORTANCE; 1065 int priority = DEFAULT_PRIORITY; 1066 int visibility = DEFAULT_VISIBILITY; 1067 boolean showBadge = DEFAULT_SHOW_BADGE; 1068 1069 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 1070 ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>(); 1071 } 1072} 1073