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 */ 16 package com.android.server.notification; 17 18 import android.app.Notification; 19 import android.content.Context; 20 import android.content.pm.PackageManager; 21 import android.content.pm.PackageManager.NameNotFoundException; 22 import android.os.UserHandle; 23 import android.service.notification.NotificationListenerService.Ranking; 24 import android.text.TextUtils; 25 import android.util.ArrayMap; 26 import android.util.Slog; 27 28 import com.android.server.notification.NotificationManagerService.DumpFilter; 29 30 import org.json.JSONArray; 31 import org.json.JSONException; 32 import org.json.JSONObject; 33 import org.xmlpull.v1.XmlPullParser; 34 import org.xmlpull.v1.XmlPullParserException; 35 import org.xmlpull.v1.XmlSerializer; 36 37 import java.io.IOException; 38 import java.io.PrintWriter; 39 import java.util.ArrayList; 40 import java.util.Collections; 41 import java.util.Map; 42 import java.util.Map.Entry; 43 44 public 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 53 private static final String ATT_NAME = "name"; 54 private static final String ATT_UID = "uid"; 55 private static final String ATT_PRIORITY = "priority"; 56 private static final String ATT_VISIBILITY = "visibility"; 57 private static final String ATT_IMPORTANCE = "importance"; 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 = Ranking.VISIBILITY_NO_OVERRIDE; 63 private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED; 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 RankingHandler mRankingHandler; 75 RankingHelper(Context context, RankingHandler rankingHandler, NotificationUsageStats usageStats, String[] extractorNames)76 public RankingHelper(Context context, RankingHandler rankingHandler, 77 NotificationUsageStats usageStats, 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") findExtractor(Class<T> extractorClass)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 extractSignals(NotificationRecord r)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 mRankingHandler.requestReconsideration(recon); 121 } 122 } catch (Throwable t) { 123 Slog.w(TAG, "NotificationSignalExtractor failed.", t); 124 } 125 } 126 } 127 readXml(XmlPullParser parser, boolean forRestore)128 public void readXml(XmlPullParser parser, boolean forRestore) 129 throws XmlPullParserException, IOException { 130 final PackageManager pm = mContext.getPackageManager(); 131 int type = parser.getEventType(); 132 if (type != XmlPullParser.START_TAG) return; 133 String tag = parser.getName(); 134 if (!TAG_RANKING.equals(tag)) return; 135 mRecords.clear(); 136 mRestoredWithoutUids.clear(); 137 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 138 tag = parser.getName(); 139 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 140 return; 141 } 142 if (type == XmlPullParser.START_TAG) { 143 if (TAG_PACKAGE.equals(tag)) { 144 int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID); 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 = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); 168 r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); 169 } 170 } 171 } 172 } 173 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 174 } 175 recordKey(String pkg, int uid)176 private static String recordKey(String pkg, int uid) { 177 return pkg + "|" + uid; 178 } 179 getOrCreateRecord(String pkg, int uid)180 private Record getOrCreateRecord(String pkg, int uid) { 181 final String key = recordKey(pkg, uid); 182 Record r = mRecords.get(key); 183 if (r == null) { 184 r = new Record(); 185 r.pkg = pkg; 186 r.uid = uid; 187 mRecords.put(key, r); 188 } 189 return r; 190 } 191 writeXml(XmlSerializer out, boolean forBackup)192 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 193 out.startTag(null, TAG_RANKING); 194 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 195 196 final int N = mRecords.size(); 197 for (int i = 0; i < N; i++) { 198 final Record r = mRecords.valueAt(i); 199 //TODO: http://b/22388012 200 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 201 continue; 202 } 203 final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE 204 || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY; 205 if (hasNonDefaultSettings) { 206 out.startTag(null, TAG_PACKAGE); 207 out.attribute(null, ATT_NAME, r.pkg); 208 if (r.importance != DEFAULT_IMPORTANCE) { 209 out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance)); 210 } 211 if (r.priority != DEFAULT_PRIORITY) { 212 out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); 213 } 214 if (r.visibility != DEFAULT_VISIBILITY) { 215 out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); 216 } 217 218 if (!forBackup) { 219 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 220 } 221 222 out.endTag(null, TAG_PACKAGE); 223 } 224 } 225 out.endTag(null, TAG_RANKING); 226 } 227 updateConfig()228 private void updateConfig() { 229 final int N = mSignalExtractors.length; 230 for (int i = 0; i < N; i++) { 231 mSignalExtractors[i].setConfig(this); 232 } 233 mRankingHandler.requestSort(); 234 } 235 sort(ArrayList<NotificationRecord> notificationList)236 public void sort(ArrayList<NotificationRecord> notificationList) { 237 final int N = notificationList.size(); 238 // clear global sort keys 239 for (int i = N - 1; i >= 0; i--) { 240 notificationList.get(i).setGlobalSortKey(null); 241 } 242 243 // rank each record individually 244 Collections.sort(notificationList, mPreliminaryComparator); 245 246 synchronized (mProxyByGroupTmp) { 247 // record individual ranking result and nominate proxies for each group 248 for (int i = N - 1; i >= 0; i--) { 249 final NotificationRecord record = notificationList.get(i); 250 record.setAuthoritativeRank(i); 251 final String groupKey = record.getGroupKey(); 252 boolean isGroupSummary = record.getNotification().isGroupSummary(); 253 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 254 mProxyByGroupTmp.put(groupKey, record); 255 } 256 } 257 // assign global sort key: 258 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 259 for (int i = 0; i < N; i++) { 260 final NotificationRecord record = notificationList.get(i); 261 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 262 String groupSortKey = record.getNotification().getSortKey(); 263 264 // We need to make sure the developer provided group sort key (gsk) is handled 265 // correctly: 266 // gsk="" < gsk=non-null-string < gsk=null 267 // 268 // We enforce this by using different prefixes for these three cases. 269 String groupSortKeyPortion; 270 if (groupSortKey == null) { 271 groupSortKeyPortion = "nsk"; 272 } else if (groupSortKey.equals("")) { 273 groupSortKeyPortion = "esk"; 274 } else { 275 groupSortKeyPortion = "gsk=" + groupSortKey; 276 } 277 278 boolean isGroupSummary = record.getNotification().isGroupSummary(); 279 record.setGlobalSortKey( 280 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 281 record.isRecentlyIntrusive() ? '0' : '1', 282 groupProxy.getAuthoritativeRank(), 283 isGroupSummary ? '0' : '1', 284 groupSortKeyPortion, 285 record.getAuthoritativeRank())); 286 } 287 mProxyByGroupTmp.clear(); 288 } 289 290 // Do a second ranking pass, using group proxies 291 Collections.sort(notificationList, mFinalComparator); 292 } 293 indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target)294 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 295 return Collections.binarySearch(notificationList, target, mFinalComparator); 296 } 297 safeInt(XmlPullParser parser, String att, int defValue)298 private static int safeInt(XmlPullParser parser, String att, int defValue) { 299 final String val = parser.getAttributeValue(null, att); 300 return tryParseInt(val, defValue); 301 } 302 tryParseInt(String value, int defValue)303 private static int tryParseInt(String value, int defValue) { 304 if (TextUtils.isEmpty(value)) return defValue; 305 try { 306 return Integer.parseInt(value); 307 } catch (NumberFormatException e) { 308 return defValue; 309 } 310 } 311 tryParseBool(String value, boolean defValue)312 private static boolean tryParseBool(String value, boolean defValue) { 313 if (TextUtils.isEmpty(value)) return defValue; 314 return Boolean.valueOf(value); 315 } 316 317 /** 318 * Gets priority. 319 */ 320 @Override getPriority(String packageName, int uid)321 public int getPriority(String packageName, int uid) { 322 return getOrCreateRecord(packageName, uid).priority; 323 } 324 325 /** 326 * Sets priority. 327 */ 328 @Override setPriority(String packageName, int uid, int priority)329 public void setPriority(String packageName, int uid, int priority) { 330 getOrCreateRecord(packageName, uid).priority = priority; 331 updateConfig(); 332 } 333 334 /** 335 * Gets visual override. 336 */ 337 @Override getVisibilityOverride(String packageName, int uid)338 public int getVisibilityOverride(String packageName, int uid) { 339 return getOrCreateRecord(packageName, uid).visibility; 340 } 341 342 /** 343 * Sets visibility override. 344 */ 345 @Override setVisibilityOverride(String pkgName, int uid, int visibility)346 public void setVisibilityOverride(String pkgName, int uid, int visibility) { 347 getOrCreateRecord(pkgName, uid).visibility = visibility; 348 updateConfig(); 349 } 350 351 /** 352 * Gets importance. 353 */ 354 @Override getImportance(String packageName, int uid)355 public int getImportance(String packageName, int uid) { 356 return getOrCreateRecord(packageName, uid).importance; 357 } 358 359 /** 360 * Sets importance. 361 */ 362 @Override setImportance(String pkgName, int uid, int importance)363 public void setImportance(String pkgName, int uid, int importance) { 364 getOrCreateRecord(pkgName, uid).importance = importance; 365 updateConfig(); 366 } 367 setEnabled(String packageName, int uid, boolean enabled)368 public void setEnabled(String packageName, int uid, boolean enabled) { 369 boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE; 370 if (wasEnabled == enabled) { 371 return; 372 } 373 setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE); 374 } 375 dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter)376 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 377 if (filter == null) { 378 final int N = mSignalExtractors.length; 379 pw.print(prefix); 380 pw.print("mSignalExtractors.length = "); 381 pw.println(N); 382 for (int i = 0; i < N; i++) { 383 pw.print(prefix); 384 pw.print(" "); 385 pw.println(mSignalExtractors[i]); 386 } 387 } 388 if (filter == null) { 389 pw.print(prefix); 390 pw.println("per-package config:"); 391 } 392 pw.println("Records:"); 393 dumpRecords(pw, prefix, filter, mRecords); 394 pw.println("Restored without uid:"); 395 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 396 } 397 dumpRecords(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records)398 private static void dumpRecords(PrintWriter pw, String prefix, 399 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 400 final int N = records.size(); 401 for (int i = 0; i < N; i++) { 402 final Record r = records.valueAt(i); 403 if (filter == null || filter.matches(r.pkg)) { 404 pw.print(prefix); 405 pw.print(" "); 406 pw.print(r.pkg); 407 pw.print(" ("); 408 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 409 pw.print(')'); 410 if (r.importance != DEFAULT_IMPORTANCE) { 411 pw.print(" importance="); 412 pw.print(Ranking.importanceToString(r.importance)); 413 } 414 if (r.priority != DEFAULT_PRIORITY) { 415 pw.print(" priority="); 416 pw.print(Notification.priorityToString(r.priority)); 417 } 418 if (r.visibility != DEFAULT_VISIBILITY) { 419 pw.print(" visibility="); 420 pw.print(Notification.visibilityToString(r.visibility)); 421 } 422 pw.println(); 423 } 424 } 425 } 426 dumpJson(NotificationManagerService.DumpFilter filter)427 public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) { 428 JSONObject ranking = new JSONObject(); 429 JSONArray records = new JSONArray(); 430 try { 431 ranking.put("noUid", mRestoredWithoutUids.size()); 432 } catch (JSONException e) { 433 // pass 434 } 435 final int N = mRecords.size(); 436 for (int i = 0; i < N; i++) { 437 final Record r = mRecords.valueAt(i); 438 if (filter == null || filter.matches(r.pkg)) { 439 JSONObject record = new JSONObject(); 440 try { 441 record.put("userId", UserHandle.getUserId(r.uid)); 442 record.put("packageName", r.pkg); 443 if (r.importance != DEFAULT_IMPORTANCE) { 444 record.put("importance", Ranking.importanceToString(r.importance)); 445 } 446 if (r.priority != DEFAULT_PRIORITY) { 447 record.put("priority", Notification.priorityToString(r.priority)); 448 } 449 if (r.visibility != DEFAULT_VISIBILITY) { 450 record.put("visibility", Notification.visibilityToString(r.visibility)); 451 } 452 } catch (JSONException e) { 453 // pass 454 } 455 records.put(record); 456 } 457 } 458 try { 459 ranking.put("records", records); 460 } catch (JSONException e) { 461 // pass 462 } 463 return ranking; 464 } 465 466 /** 467 * Dump only the ban information as structured JSON for the stats collector. 468 * 469 * This is intentionally redundant with {#link dumpJson} because the old 470 * scraper will expect this format. 471 * 472 * @param filter 473 * @return 474 */ dumpBansJson(NotificationManagerService.DumpFilter filter)475 public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) { 476 JSONArray bans = new JSONArray(); 477 Map<Integer, String> packageBans = getPackageBans(); 478 for(Entry<Integer, String> ban : packageBans.entrySet()) { 479 final int userId = UserHandle.getUserId(ban.getKey()); 480 final String packageName = ban.getValue(); 481 if (filter == null || filter.matches(packageName)) { 482 JSONObject banJson = new JSONObject(); 483 try { 484 banJson.put("userId", userId); 485 banJson.put("packageName", packageName); 486 } catch (JSONException e) { 487 e.printStackTrace(); 488 } 489 bans.put(banJson); 490 } 491 } 492 return bans; 493 } 494 getPackageBans()495 public Map<Integer, String> getPackageBans() { 496 final int N = mRecords.size(); 497 ArrayMap<Integer, String> packageBans = new ArrayMap<>(N); 498 for (int i = 0; i < N; i++) { 499 final Record r = mRecords.valueAt(i); 500 if (r.importance == Ranking.IMPORTANCE_NONE) { 501 packageBans.put(r.uid, r.pkg); 502 } 503 } 504 return packageBans; 505 } 506 onPackagesChanged(boolean removingPackage, String[] pkgList)507 public void onPackagesChanged(boolean removingPackage, String[] pkgList) { 508 if (!removingPackage || pkgList == null || pkgList.length == 0 509 || mRestoredWithoutUids.isEmpty()) { 510 return; // nothing to do 511 } 512 final PackageManager pm = mContext.getPackageManager(); 513 boolean updated = false; 514 for (String pkg : pkgList) { 515 final Record r = mRestoredWithoutUids.get(pkg); 516 if (r != null) { 517 try { 518 //TODO: http://b/22388012 519 r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM); 520 mRestoredWithoutUids.remove(pkg); 521 mRecords.put(recordKey(r.pkg, r.uid), r); 522 updated = true; 523 } catch (NameNotFoundException e) { 524 // noop 525 } 526 } 527 } 528 if (updated) { 529 updateConfig(); 530 } 531 } 532 533 private static class Record { 534 static int UNKNOWN_UID = UserHandle.USER_NULL; 535 536 String pkg; 537 int uid = UNKNOWN_UID; 538 int importance = DEFAULT_IMPORTANCE; 539 int priority = DEFAULT_PRIORITY; 540 int visibility = DEFAULT_VISIBILITY; 541 } 542 } 543