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