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