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