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