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