RankingHelper.java revision 5d25ee7a006fda7150ba251cff92c27130611f88
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 174 // Migrate package level settings to the default topic. 175 // Might be overwritten by parseTopics. 176 Topic defaultTopic = r.topics.get(Notification.TOPIC_DEFAULT); 177 defaultTopic.priority = priority; 178 defaultTopic.visibility = vis; 179 180 parseTopics(r, parser); 181 } 182 } 183 } 184 } 185 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 186 } 187 188 public void parseTopics(Record r, XmlPullParser parser) 189 throws XmlPullParserException, IOException { 190 final int innerDepth = parser.getDepth(); 191 int type; 192 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 193 && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { 194 if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { 195 continue; 196 } 197 198 String tagName = parser.getName(); 199 if (TAG_TOPIC.equals(tagName)) { 200 int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); 201 int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); 202 int importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE); 203 String id = parser.getAttributeValue(null, ATT_TOPIC_ID); 204 CharSequence label = parser.getAttributeValue(null, ATT_TOPIC_LABEL); 205 206 if (!TextUtils.isEmpty(id)) { 207 Topic topic = new Topic(new Notification.Topic(id, label)); 208 209 if (priority != DEFAULT_PRIORITY) { 210 topic.priority = priority; 211 } 212 if (vis != DEFAULT_VISIBILITY) { 213 topic.visibility = vis; 214 } 215 if (importance != DEFAULT_IMPORTANCE) { 216 topic.importance = importance; 217 } 218 r.topics.put(id, topic); 219 } 220 } 221 } 222 } 223 224 private static String recordKey(String pkg, int uid) { 225 return pkg + "|" + uid; 226 } 227 228 private Record getOrCreateRecord(String pkg, int uid) { 229 final String key = recordKey(pkg, uid); 230 Record r = mRecords.get(key); 231 if (r == null) { 232 r = new Record(); 233 r.pkg = pkg; 234 r.uid = uid; 235 r.topics.put(Notification.TOPIC_DEFAULT, new Topic(createDefaultTopic())); 236 mRecords.put(key, r); 237 } 238 return r; 239 } 240 241 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 242 out.startTag(null, TAG_RANKING); 243 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 244 245 final int N = mRecords.size(); 246 for (int i = 0; i < N; i++) { 247 final Record r = mRecords.valueAt(i); 248 //TODO: http://b/22388012 249 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 250 continue; 251 } 252 out.startTag(null, TAG_PACKAGE); 253 out.attribute(null, ATT_NAME, r.pkg); 254 255 if (!forBackup) { 256 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 257 } 258 259 writeTopicsXml(out, r); 260 out.endTag(null, TAG_PACKAGE); 261 } 262 out.endTag(null, TAG_RANKING); 263 } 264 265 public void writeTopicsXml(XmlSerializer out, Record r) throws IOException { 266 for (Topic t : r.topics.values()) { 267 out.startTag(null, TAG_TOPIC); 268 out.attribute(null, ATT_TOPIC_ID, t.topic.getId()); 269 out.attribute(null, ATT_TOPIC_LABEL, t.topic.getLabel().toString()); 270 if (t.priority != DEFAULT_PRIORITY) { 271 out.attribute(null, ATT_PRIORITY, Integer.toString(t.priority)); 272 } 273 if (t.visibility != DEFAULT_VISIBILITY) { 274 out.attribute(null, ATT_VISIBILITY, Integer.toString(t.visibility)); 275 } 276 if (t.importance != DEFAULT_IMPORTANCE) { 277 out.attribute(null, ATT_IMPORTANCE, Integer.toString(t.importance)); 278 } 279 out.endTag(null, TAG_TOPIC); 280 } 281 } 282 283 private void updateConfig() { 284 final int N = mSignalExtractors.length; 285 for (int i = 0; i < N; i++) { 286 mSignalExtractors[i].setConfig(this); 287 } 288 mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE); 289 } 290 291 public void sort(ArrayList<NotificationRecord> notificationList) { 292 final int N = notificationList.size(); 293 // clear global sort keys 294 for (int i = N - 1; i >= 0; i--) { 295 notificationList.get(i).setGlobalSortKey(null); 296 } 297 298 // rank each record individually 299 Collections.sort(notificationList, mPreliminaryComparator); 300 301 synchronized (mProxyByGroupTmp) { 302 // record individual ranking result and nominate proxies for each group 303 for (int i = N - 1; i >= 0; i--) { 304 final NotificationRecord record = notificationList.get(i); 305 record.setAuthoritativeRank(i); 306 final String groupKey = record.getGroupKey(); 307 boolean isGroupSummary = record.getNotification().isGroupSummary(); 308 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 309 mProxyByGroupTmp.put(groupKey, record); 310 } 311 } 312 // assign global sort key: 313 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 314 for (int i = 0; i < N; i++) { 315 final NotificationRecord record = notificationList.get(i); 316 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 317 String groupSortKey = record.getNotification().getSortKey(); 318 319 // We need to make sure the developer provided group sort key (gsk) is handled 320 // correctly: 321 // gsk="" < gsk=non-null-string < gsk=null 322 // 323 // We enforce this by using different prefixes for these three cases. 324 String groupSortKeyPortion; 325 if (groupSortKey == null) { 326 groupSortKeyPortion = "nsk"; 327 } else if (groupSortKey.equals("")) { 328 groupSortKeyPortion = "esk"; 329 } else { 330 groupSortKeyPortion = "gsk=" + groupSortKey; 331 } 332 333 boolean isGroupSummary = record.getNotification().isGroupSummary(); 334 record.setGlobalSortKey( 335 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 336 record.isRecentlyIntrusive() ? '0' : '1', 337 groupProxy.getAuthoritativeRank(), 338 isGroupSummary ? '0' : '1', 339 groupSortKeyPortion, 340 record.getAuthoritativeRank())); 341 } 342 mProxyByGroupTmp.clear(); 343 } 344 345 // Do a second ranking pass, using group proxies 346 Collections.sort(notificationList, mFinalComparator); 347 } 348 349 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 350 return Collections.binarySearch(notificationList, target, mFinalComparator); 351 } 352 353 private static int safeInt(XmlPullParser parser, String att, int defValue) { 354 final String val = parser.getAttributeValue(null, att); 355 return tryParseInt(val, defValue); 356 } 357 358 private static int tryParseInt(String value, int defValue) { 359 if (TextUtils.isEmpty(value)) return defValue; 360 try { 361 return Integer.valueOf(value); 362 } catch (NumberFormatException e) { 363 return defValue; 364 } 365 } 366 367 private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { 368 final String val = parser.getAttributeValue(null, att); 369 return tryParseBool(val, defValue); 370 } 371 372 private static boolean tryParseBool(String value, boolean defValue) { 373 if (TextUtils.isEmpty(value)) return defValue; 374 return Boolean.valueOf(value); 375 } 376 377 @Override 378 public List<Notification.Topic> getTopics(String packageName, int uid) { 379 final Record r = getOrCreateRecord(packageName, uid); 380 List<Notification.Topic> topics = new ArrayList<>(); 381 for (Topic t : r.topics.values()) { 382 topics.add(t.topic); 383 } 384 return topics; 385 } 386 387 @Override 388 public int getTopicPriority(String packageName, int uid, Notification.Topic topic) { 389 final Record r = getOrCreateRecord(packageName, uid); 390 return getOrCreateTopic(r, topic).priority; 391 } 392 393 @Override 394 public void setTopicPriority(String packageName, int uid, Notification.Topic topic, 395 int priority) { 396 final Record r = getOrCreateRecord(packageName, uid); 397 getOrCreateTopic(r, topic).priority = priority; 398 updateConfig(); 399 } 400 401 @Override 402 public int getTopicVisibilityOverride(String packageName, int uid, Notification.Topic topic) { 403 final Record r = getOrCreateRecord(packageName, uid); 404 return getOrCreateTopic(r, topic).visibility; 405 } 406 407 @Override 408 public void setTopicVisibilityOverride(String pkgName, int uid, Notification.Topic topic, 409 int visibility) { 410 final Record r = getOrCreateRecord(pkgName, uid); 411 getOrCreateTopic(r, topic).visibility = visibility; 412 updateConfig(); 413 } 414 415 @Override 416 public int getTopicImportance(String packageName, int uid, Notification.Topic topic) { 417 final Record r = getOrCreateRecord(packageName, uid); 418 return getOrCreateTopic(r, topic).importance; 419 } 420 421 @Override 422 public void setTopicImportance(String pkgName, int uid, Notification.Topic topic, 423 int importance) { 424 final Record r = getOrCreateRecord(pkgName, uid); 425 getOrCreateTopic(r, topic).importance = importance; 426 updateConfig(); 427 } 428 429 private Topic getOrCreateTopic(Record r, Notification.Topic topic) { 430 if (topic == null) { 431 topic = createDefaultTopic(); 432 } 433 Topic t = r.topics.get(topic.getId()); 434 if (t != null) { 435 return t; 436 } else { 437 t = new Topic(topic); 438 r.topics.put(topic.getId(), t); 439 return t; 440 } 441 } 442 443 private Notification.Topic createDefaultTopic() { 444 return new Notification.Topic(Notification.TOPIC_DEFAULT, 445 mContext.getString(R.string.default_notification_topic_label)); 446 } 447 448 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 449 if (filter == null) { 450 final int N = mSignalExtractors.length; 451 pw.print(prefix); 452 pw.print("mSignalExtractors.length = "); 453 pw.println(N); 454 for (int i = 0; i < N; i++) { 455 pw.print(prefix); 456 pw.print(" "); 457 pw.println(mSignalExtractors[i]); 458 } 459 } 460 if (filter == null) { 461 pw.print(prefix); 462 pw.println("per-package config:"); 463 } 464 dumpRecords(pw, prefix, filter, mRecords); 465 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 466 } 467 468 private static void dumpRecords(PrintWriter pw, String prefix, 469 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 470 final int N = records.size(); 471 for (int i = 0; i < N; i++) { 472 final Record r = records.valueAt(i); 473 if (filter == null || filter.matches(r.pkg)) { 474 pw.print(prefix); 475 pw.print(" "); 476 pw.print(r.pkg); 477 pw.print(" ("); 478 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 479 pw.print(')'); 480 pw.println(); 481 for (Topic t : r.topics.values()) { 482 pw.print(prefix); 483 pw.print(" "); 484 pw.print(" "); 485 pw.print(t.topic.getId()); 486 if (t.priority != DEFAULT_PRIORITY) { 487 pw.print(" priority="); 488 pw.print(Notification.priorityToString(t.priority)); 489 } 490 if (t.visibility != DEFAULT_VISIBILITY) { 491 pw.print(" visibility="); 492 pw.print(Notification.visibilityToString(t.visibility)); 493 } 494 if (t.importance != DEFAULT_IMPORTANCE) { 495 pw.print(" importance="); 496 pw.print(Ranking.importanceToString(t.importance)); 497 } 498 pw.println(); 499 } 500 } 501 } 502 } 503 504 public void onPackagesChanged(boolean queryReplace, String[] pkgList) { 505 if (queryReplace || pkgList == null || pkgList.length == 0 506 || mRestoredWithoutUids.isEmpty()) { 507 return; // nothing to do 508 } 509 final PackageManager pm = mContext.getPackageManager(); 510 boolean updated = false; 511 for (String pkg : pkgList) { 512 final Record r = mRestoredWithoutUids.get(pkg); 513 if (r != null) { 514 try { 515 //TODO: http://b/22388012 516 r.uid = pm.getPackageUid(r.pkg, UserHandle.USER_SYSTEM); 517 mRestoredWithoutUids.remove(pkg); 518 mRecords.put(recordKey(r.pkg, r.uid), r); 519 updated = true; 520 } catch (NameNotFoundException e) { 521 // noop 522 } 523 } 524 } 525 if (updated) { 526 updateConfig(); 527 } 528 } 529 530 private static class Record { 531 static int UNKNOWN_UID = UserHandle.USER_NULL; 532 533 String pkg; 534 int uid = UNKNOWN_UID; 535 Map<String, Topic> topics = new ArrayMap<>(); 536 } 537 538 private static class Topic { 539 Notification.Topic topic; 540 int priority = DEFAULT_PRIORITY; 541 int visibility = DEFAULT_VISIBILITY; 542 int importance = DEFAULT_IMPORTANCE; 543 544 public Topic(Notification.Topic topic) { 545 this.topic = topic; 546 } 547 } 548} 549