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