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