RankingHelper.java revision 808913e008b1038d7f6ad5442ae203ada7290a81
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 android.app.Notification; 19import android.app.NotificationChannel; 20import android.content.Context; 21import android.content.pm.PackageManager; 22import android.content.pm.PackageManager.NameNotFoundException; 23import android.content.pm.ParceledListSlice; 24import android.os.Process; 25import android.os.UserHandle; 26import android.service.notification.NotificationListenerService.Ranking; 27import android.text.TextUtils; 28import android.util.ArrayMap; 29import android.util.Slog; 30 31import com.android.internal.R; 32 33import org.json.JSONArray; 34import org.json.JSONException; 35import org.json.JSONObject; 36import org.xmlpull.v1.XmlPullParser; 37import org.xmlpull.v1.XmlPullParserException; 38import org.xmlpull.v1.XmlSerializer; 39 40import java.io.IOException; 41import java.io.PrintWriter; 42import java.util.ArrayList; 43import java.util.Collections; 44import java.util.List; 45import java.util.Map; 46import java.util.Map.Entry; 47 48public class RankingHelper implements RankingConfig { 49 private static final String TAG = "RankingHelper"; 50 51 private static final int XML_VERSION = 1; 52 53 private static final String TAG_RANKING = "ranking"; 54 private static final String TAG_PACKAGE = "package"; 55 private static final String TAG_CHANNEL = "channel"; 56 57 private static final String ATT_VERSION = "version"; 58 private static final String ATT_NAME = "name"; 59 private static final String ATT_UID = "uid"; 60 private static final String ATT_ID = "id"; 61 private static final String ATT_PRIORITY = "priority"; 62 private static final String ATT_VISIBILITY = "visibility"; 63 private static final String ATT_IMPORTANCE = "importance"; 64 65 private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; 66 private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE; 67 private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED; 68 69 private final NotificationSignalExtractor[] mSignalExtractors; 70 private final NotificationComparator mPreliminaryComparator = new NotificationComparator(); 71 private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator(); 72 73 private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record 74 private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>(); 75 private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record 76 77 private final Context mContext; 78 private final RankingHandler mRankingHandler; 79 80 public RankingHelper(Context context, RankingHandler rankingHandler, 81 NotificationUsageStats usageStats, String[] extractorNames) { 82 mContext = context; 83 mRankingHandler = rankingHandler; 84 85 final int N = extractorNames.length; 86 mSignalExtractors = new NotificationSignalExtractor[N]; 87 for (int i = 0; i < N; i++) { 88 try { 89 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]); 90 NotificationSignalExtractor extractor = 91 (NotificationSignalExtractor) extractorClass.newInstance(); 92 extractor.initialize(mContext, usageStats); 93 extractor.setConfig(this); 94 mSignalExtractors[i] = extractor; 95 } catch (ClassNotFoundException e) { 96 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e); 97 } catch (InstantiationException e) { 98 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e); 99 } catch (IllegalAccessException e) { 100 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e); 101 } 102 } 103 } 104 105 @SuppressWarnings("unchecked") 106 public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) { 107 final int N = mSignalExtractors.length; 108 for (int i = 0; i < N; i++) { 109 final NotificationSignalExtractor extractor = mSignalExtractors[i]; 110 if (extractorClass.equals(extractor.getClass())) { 111 return (T) extractor; 112 } 113 } 114 return null; 115 } 116 117 public void extractSignals(NotificationRecord r) { 118 final int N = mSignalExtractors.length; 119 for (int i = 0; i < N; i++) { 120 NotificationSignalExtractor extractor = mSignalExtractors[i]; 121 try { 122 RankingReconsideration recon = extractor.process(r); 123 if (recon != null) { 124 mRankingHandler.requestReconsideration(recon); 125 } 126 } catch (Throwable t) { 127 Slog.w(TAG, "NotificationSignalExtractor failed.", t); 128 } 129 } 130 } 131 132 public void readXml(XmlPullParser parser, boolean forRestore) 133 throws XmlPullParserException, IOException { 134 final PackageManager pm = mContext.getPackageManager(); 135 int type = parser.getEventType(); 136 if (type != XmlPullParser.START_TAG) return; 137 String tag = parser.getName(); 138 if (!TAG_RANKING.equals(tag)) return; 139 mRecords.clear(); 140 mRestoredWithoutUids.clear(); 141 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 142 tag = parser.getName(); 143 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 144 return; 145 } 146 if (type == XmlPullParser.START_TAG) { 147 if (TAG_PACKAGE.equals(tag)) { 148 int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID); 149 String name = parser.getAttributeValue(null, ATT_NAME); 150 151 if (!TextUtils.isEmpty(name)) { 152 if (forRestore) { 153 try { 154 //TODO: http://b/22388012 155 uid = pm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM); 156 } catch (NameNotFoundException e) { 157 // noop 158 } 159 } 160 Record r = null; 161 if (uid == Record.UNKNOWN_UID) { 162 r = mRestoredWithoutUids.get(name); 163 if (r == null) { 164 r = new Record(); 165 r.pkg = name; 166 mRestoredWithoutUids.put(name, r); 167 } 168 } else { 169 r = getOrCreateRecord(name, uid); 170 } 171 r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); 172 r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); 173 r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); 174 175 final int innerDepth = parser.getDepth(); 176 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 177 && (type != XmlPullParser.END_TAG 178 || parser.getDepth() > innerDepth)) { 179 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 180 continue; 181 } 182 183 String tagName = parser.getName(); 184 if (TAG_CHANNEL.equals(tagName)) { 185 String id = parser.getAttributeValue(null, ATT_ID); 186 CharSequence channelName = parser.getAttributeValue(null, ATT_NAME); 187 188 if (!TextUtils.isEmpty(id)) { 189 final NotificationChannel channel = 190 new NotificationChannel(id, channelName); 191 channel.populateFromXml(parser); 192 r.channels.put(id, channel); 193 } 194 } 195 } 196 } 197 } 198 } 199 } 200 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 201 } 202 203 private static String recordKey(String pkg, int uid) { 204 return pkg + "|" + uid; 205 } 206 207 private Record getOrCreateRecord(String pkg, int uid) { 208 final String key = recordKey(pkg, uid); 209 Record r = mRecords.get(key); 210 if (r == null) { 211 r = new Record(); 212 r.pkg = pkg; 213 r.uid = uid; 214 NotificationChannel defaultChannel = createDefaultChannel(); 215 r.channels.put(defaultChannel.getId(), defaultChannel); 216 mRecords.put(key, r); 217 } 218 return r; 219 } 220 221 private NotificationChannel createDefaultChannel() { 222 return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID, 223 mContext.getString(R.string.default_notification_channel_label)); 224 } 225 226 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 227 out.startTag(null, TAG_RANKING); 228 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 229 230 final int N = mRecords.size(); 231 for (int i = 0; i < N; i++) { 232 final Record r = mRecords.valueAt(i); 233 //TODO: http://b/22388012 234 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 235 continue; 236 } 237 final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE 238 || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY 239 || r.channels.size() > 0; 240 if (hasNonDefaultSettings) { 241 out.startTag(null, TAG_PACKAGE); 242 out.attribute(null, ATT_NAME, r.pkg); 243 if (r.importance != DEFAULT_IMPORTANCE) { 244 out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); 245 } 246 if (r.priority != DEFAULT_PRIORITY) { 247 out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); 248 } 249 if (r.visibility != DEFAULT_VISIBILITY) { 250 out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); 251 } 252 253 if (!forBackup) { 254 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 255 } 256 257 for (NotificationChannel channel : r.channels.values()) { 258 channel.writeXml(out); 259 } 260 261 out.endTag(null, TAG_PACKAGE); 262 } 263 } 264 out.endTag(null, TAG_RANKING); 265 } 266 267 private void updateConfig() { 268 final int N = mSignalExtractors.length; 269 for (int i = 0; i < N; i++) { 270 mSignalExtractors[i].setConfig(this); 271 } 272 mRankingHandler.requestSort(); 273 } 274 275 public void sort(ArrayList<NotificationRecord> notificationList) { 276 final int N = notificationList.size(); 277 // clear global sort keys 278 for (int i = N - 1; i >= 0; i--) { 279 notificationList.get(i).setGlobalSortKey(null); 280 } 281 282 // rank each record individually 283 Collections.sort(notificationList, mPreliminaryComparator); 284 285 synchronized (mProxyByGroupTmp) { 286 // record individual ranking result and nominate proxies for each group 287 for (int i = N - 1; i >= 0; i--) { 288 final NotificationRecord record = notificationList.get(i); 289 record.setAuthoritativeRank(i); 290 final String groupKey = record.getGroupKey(); 291 boolean isGroupSummary = record.getNotification().isGroupSummary(); 292 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 293 mProxyByGroupTmp.put(groupKey, record); 294 } 295 } 296 // assign global sort key: 297 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 298 for (int i = 0; i < N; i++) { 299 final NotificationRecord record = notificationList.get(i); 300 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 301 String groupSortKey = record.getNotification().getSortKey(); 302 303 // We need to make sure the developer provided group sort key (gsk) is handled 304 // correctly: 305 // gsk="" < gsk=non-null-string < gsk=null 306 // 307 // We enforce this by using different prefixes for these three cases. 308 String groupSortKeyPortion; 309 if (groupSortKey == null) { 310 groupSortKeyPortion = "nsk"; 311 } else if (groupSortKey.equals("")) { 312 groupSortKeyPortion = "esk"; 313 } else { 314 groupSortKeyPortion = "gsk=" + groupSortKey; 315 } 316 317 boolean isGroupSummary = record.getNotification().isGroupSummary(); 318 record.setGlobalSortKey( 319 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 320 record.isRecentlyIntrusive() ? '0' : '1', 321 groupProxy.getAuthoritativeRank(), 322 isGroupSummary ? '0' : '1', 323 groupSortKeyPortion, 324 record.getAuthoritativeRank())); 325 } 326 mProxyByGroupTmp.clear(); 327 } 328 329 // Do a second ranking pass, using group proxies 330 Collections.sort(notificationList, mFinalComparator); 331 } 332 333 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 334 return Collections.binarySearch(notificationList, target, mFinalComparator); 335 } 336 337 private static int safeInt(XmlPullParser parser, String att, int defValue) { 338 final String val = parser.getAttributeValue(null, att); 339 return tryParseInt(val, defValue); 340 } 341 342 private static int tryParseInt(String value, int defValue) { 343 if (TextUtils.isEmpty(value)) return defValue; 344 try { 345 return Integer.parseInt(value); 346 } catch (NumberFormatException e) { 347 return defValue; 348 } 349 } 350 351 /** 352 * Gets priority. 353 */ 354 @Override 355 public int getPriority(String packageName, int uid) { 356 return getOrCreateRecord(packageName, uid).priority; 357 } 358 359 /** 360 * Sets priority. 361 */ 362 @Override 363 public void setPriority(String packageName, int uid, int priority) { 364 getOrCreateRecord(packageName, uid).priority = priority; 365 updateConfig(); 366 } 367 368 /** 369 * Gets visual override. 370 */ 371 @Override 372 public int getVisibilityOverride(String packageName, int uid) { 373 return getOrCreateRecord(packageName, uid).visibility; 374 } 375 376 /** 377 * Sets visibility override. 378 */ 379 @Override 380 public void setVisibilityOverride(String pkgName, int uid, int visibility) { 381 getOrCreateRecord(pkgName, uid).visibility = visibility; 382 updateConfig(); 383 } 384 385 /** 386 * Gets importance. 387 */ 388 @Override 389 public int getImportance(String packageName, int uid) { 390 return getOrCreateRecord(packageName, uid).importance; 391 } 392 393 @Override 394 public void createNotificationChannel(String pkg, int uid, NotificationChannel channel) { 395 Record r = getOrCreateRecord(pkg, uid); 396 if (r.channels.containsKey(channel.getId()) || channel.getName().equals( 397 mContext.getString(R.string.default_notification_channel_label))) { 398 throw new IllegalArgumentException("Channel already exists"); 399 } 400 if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 401 channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 402 } 403 r.channels.put(channel.getId(), channel); 404 updateConfig(); 405 } 406 407 @Override 408 public void updateNotificationChannel(int callingUid, String pkg, int uid, 409 NotificationChannel updatedChannel) { 410 Record r = getOrCreateRecord(pkg, uid); 411 NotificationChannel channel = r.channels.get(updatedChannel.getId()); 412 if (channel == null) { 413 throw new IllegalArgumentException("Channel does not exist"); 414 } 415 if (!isUidSystem(callingUid)) { 416 updatedChannel.setImportance(channel.getImportance()); 417 updatedChannel.setName(channel.getName()); 418 } 419 if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) { 420 updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE); 421 } 422 r.channels.put(updatedChannel.getId(), updatedChannel); 423 updateConfig(); 424 } 425 426 @Override 427 public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId) { 428 Record r = getOrCreateRecord(pkg, uid); 429 if (channelId == null) { 430 channelId = NotificationChannel.DEFAULT_CHANNEL_ID; 431 } 432 return r.channels.get(channelId); 433 } 434 435 @Override 436 public void deleteNotificationChannel(String pkg, int uid, String channelId) { 437 Record r = getOrCreateRecord(pkg, uid); 438 r.channels.remove(channelId); 439 } 440 441 @Override 442 public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid) { 443 List<NotificationChannel> channels = new ArrayList<>(); 444 Record r = getOrCreateRecord(pkg, uid); 445 int N = r.channels.size(); 446 for (int i = 0; i < N; i++) { 447 channels.add(r.channels.valueAt(i)); 448 } 449 return new ParceledListSlice<NotificationChannel>(channels); 450 } 451 452 /** 453 * Sets importance. 454 */ 455 @Override 456 public void setImportance(String pkgName, int uid, int importance) { 457 getOrCreateRecord(pkgName, uid).importance = importance; 458 updateConfig(); 459 } 460 461 public void setEnabled(String packageName, int uid, boolean enabled) { 462 boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE; 463 if (wasEnabled == enabled) { 464 return; 465 } 466 setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE); 467 } 468 469 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 470 if (filter == null) { 471 final int N = mSignalExtractors.length; 472 pw.print(prefix); 473 pw.print("mSignalExtractors.length = "); 474 pw.println(N); 475 for (int i = 0; i < N; i++) { 476 pw.print(prefix); 477 pw.print(" "); 478 pw.println(mSignalExtractors[i]); 479 } 480 } 481 if (filter == null) { 482 pw.print(prefix); 483 pw.println("per-package config:"); 484 } 485 pw.println("Records:"); 486 dumpRecords(pw, prefix, filter, mRecords); 487 pw.println("Restored without uid:"); 488 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 489 } 490 491 private static void dumpRecords(PrintWriter pw, String prefix, 492 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 493 final int N = records.size(); 494 for (int i = 0; i < N; i++) { 495 final Record r = records.valueAt(i); 496 if (filter == null || filter.matches(r.pkg)) { 497 pw.print(prefix); 498 pw.print(" "); 499 pw.print(r.pkg); 500 pw.print(" ("); 501 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 502 pw.print(')'); 503 if (r.importance != DEFAULT_IMPORTANCE) { 504 pw.print(" importance="); 505 pw.print(Ranking.importanceToString(r.importance)); 506 } 507 if (r.priority != DEFAULT_PRIORITY) { 508 pw.print(" priority="); 509 pw.print(Notification.priorityToString(r.priority)); 510 } 511 if (r.visibility != DEFAULT_VISIBILITY) { 512 pw.print(" visibility="); 513 pw.print(Notification.visibilityToString(r.visibility)); 514 } 515 pw.println(); 516 for (NotificationChannel channel : r.channels.values()) { 517 pw.print(prefix); 518 pw.print(" "); 519 pw.print(" "); 520 pw.println(channel); 521 } 522 } 523 } 524 } 525 526 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 527 JSONObject ranking = new JSONObject(); 528 JSONArray records = new JSONArray(); 529 try { 530 ranking.put("noUid", mRestoredWithoutUids.size()); 531 } catch (JSONException e) { 532 // pass 533 } 534 final int N = mRecords.size(); 535 for (int i = 0; i < N; i++) { 536 final Record r = mRecords.valueAt(i); 537 if (filter == null || filter.matches(r.pkg)) { 538 JSONObject record = new JSONObject(); 539 try { 540 record.put("userId", UserHandle.getUserId(r.uid)); 541 record.put("packageName", r.pkg); 542 if (r.importance != DEFAULT_IMPORTANCE) { 543 record.put("importance", Ranking.importanceToString(r.importance)); 544 } 545 if (r.priority != DEFAULT_PRIORITY) { 546 record.put("priority", Notification.priorityToString(r.priority)); 547 } 548 if (r.visibility != DEFAULT_VISIBILITY) { 549 record.put("visibility", Notification.visibilityToString(r.visibility)); 550 } 551 for (NotificationChannel channel : r.channels.values()) { 552 record.put("channel", channel.toJson()); 553 } 554 } catch (JSONException e) { 555 // pass 556 } 557 records.put(record); 558 } 559 } 560 try { 561 ranking.put("records", records); 562 } catch (JSONException e) { 563 // pass 564 } 565 return ranking; 566 } 567 568 /** 569 * Dump only the ban information as structured JSON for the stats collector. 570 * 571 * This is intentionally redundant with {#link dumpJson} because the old 572 * scraper will expect this format. 573 * 574 * @param filter 575 * @return 576 */ 577 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 578 JSONArray bans = new JSONArray(); 579 Map<Integer, String> packageBans = getPackageBans(); 580 for(Entry<Integer, String> ban : packageBans.entrySet()) { 581 final int userId = UserHandle.getUserId(ban.getKey()); 582 final String packageName = ban.getValue(); 583 if (filter == null || filter.matches(packageName)) { 584 JSONObject banJson = new JSONObject(); 585 try { 586 banJson.put("userId", userId); 587 banJson.put("packageName", packageName); 588 } catch (JSONException e) { 589 e.printStackTrace(); 590 } 591 bans.put(banJson); 592 } 593 } 594 return bans; 595 } 596 597 public Map<Integer, String> getPackageBans() { 598 final int N = mRecords.size(); 599 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 600 for (int i = 0; i < N; i++) { 601 final Record r = mRecords.valueAt(i); 602 if (r.importance == Ranking.IMPORTANCE_NONE) { 603 packageBans.put(r.uid, r.pkg); 604 } 605 } 606 return packageBans; 607 } 608 609 public void onPackagesChanged(boolean removingPackage, String[] pkgList) { 610 if (removingPackage || pkgList == null || pkgList.length == 0 611 || mRestoredWithoutUids.isEmpty()) { 612 return; // nothing to do 613 } 614 final PackageManager pm = mContext.getPackageManager(); 615 boolean updated = false; 616 for (String pkg : pkgList) { 617 final Record r = mRestoredWithoutUids.get(pkg); 618 if (r != null) { 619 try { 620 //TODO: http://b/22388012 621 r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM); 622 mRestoredWithoutUids.remove(pkg); 623 mRecords.put(recordKey(r.pkg, r.uid), r); 624 updated = true; 625 } catch (NameNotFoundException e) { 626 // noop 627 } 628 } 629 } 630 if (updated) { 631 updateConfig(); 632 } 633 } 634 635 private static boolean isUidSystem(int uid) { 636 final int appid = UserHandle.getAppId(uid); 637 return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); 638 } 639 640 private static class Record { 641 static int UNKNOWN_UID = UserHandle.USER_NULL; 642 643 String pkg; 644 int uid = UNKNOWN_UID; 645 int importance = DEFAULT_IMPORTANCE; 646 int priority = DEFAULT_PRIORITY; 647 int visibility = DEFAULT_VISIBILITY; 648 649 ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); 650 } 651} 652