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