RankingHelper.java revision bc253a28582c4cdd56cc8a0d49cd69b4624cfb4b
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_VISIBILITY = "visibility"; 53 54 private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT; 55 private static final int DEFAULT_VISIBILITY = 56 NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE; 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 Handler mRankingHandler; 68 69 public RankingHelper(Context context, Handler rankingHandler, NotificationUsageStats usageStats, 70 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 Message m = Message.obtain(mRankingHandler, 114 NotificationManagerService.MESSAGE_RECONSIDER_RANKING, recon); 115 long delay = recon.getDelay(TimeUnit.MILLISECONDS); 116 mRankingHandler.sendMessageDelayed(m, delay); 117 } 118 } catch (Throwable t) { 119 Slog.w(TAG, "NotificationSignalExtractor failed.", t); 120 } 121 } 122 } 123 124 public void readXml(XmlPullParser parser, boolean forRestore) 125 throws XmlPullParserException, IOException { 126 final PackageManager pm = mContext.getPackageManager(); 127 int type = parser.getEventType(); 128 if (type != XmlPullParser.START_TAG) return; 129 String tag = parser.getName(); 130 if (!TAG_RANKING.equals(tag)) return; 131 mRecords.clear(); 132 mRestoredWithoutUids.clear(); 133 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 134 tag = parser.getName(); 135 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 136 return; 137 } 138 if (type == XmlPullParser.START_TAG) { 139 if (TAG_PACKAGE.equals(tag)) { 140 int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID); 141 int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY); 142 int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY); 143 String name = parser.getAttributeValue(null, ATT_NAME); 144 145 if (!TextUtils.isEmpty(name)) { 146 if (forRestore) { 147 try { 148 //TODO: http://b/22388012 149 uid = pm.getPackageUid(name, UserHandle.USER_SYSTEM); 150 } catch (NameNotFoundException e) { 151 // noop 152 } 153 } 154 Record r = null; 155 if (uid == Record.UNKNOWN_UID) { 156 r = mRestoredWithoutUids.get(name); 157 if (r == null) { 158 r = new Record(); 159 mRestoredWithoutUids.put(name, r); 160 } 161 } else { 162 r = getOrCreateRecord(name, uid); 163 } 164 if (priority != DEFAULT_PRIORITY) { 165 r.priority = priority; 166 } 167 if (vis != DEFAULT_VISIBILITY) { 168 r.visibility = vis; 169 } 170 } 171 } 172 } 173 } 174 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 175 } 176 177 private static String recordKey(String pkg, int uid) { 178 return pkg + "|" + uid; 179 } 180 181 private Record getOrCreateRecord(String pkg, int uid) { 182 final String key = recordKey(pkg, uid); 183 Record r = mRecords.get(key); 184 if (r == null) { 185 r = new Record(); 186 r.pkg = pkg; 187 r.uid = uid; 188 mRecords.put(key, r); 189 } 190 return r; 191 } 192 193 private void removeDefaultRecords() { 194 final int N = mRecords.size(); 195 for (int i = N - 1; i >= 0; i--) { 196 final Record r = mRecords.valueAt(i); 197 if (r.priority == DEFAULT_PRIORITY && r.visibility == DEFAULT_VISIBILITY) { 198 mRecords.remove(i); 199 } 200 } 201 } 202 203 public void writeXml(XmlSerializer out, boolean forBackup) throws IOException { 204 out.startTag(null, TAG_RANKING); 205 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 206 207 final int N = mRecords.size(); 208 for (int i = 0; i < N; i++) { 209 final Record r = mRecords.valueAt(i); 210 //TODO: http://b/22388012 211 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) { 212 continue; 213 } 214 out.startTag(null, TAG_PACKAGE); 215 out.attribute(null, ATT_NAME, r.pkg); 216 if (r.priority != DEFAULT_PRIORITY) { 217 out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority)); 218 } 219 if (r.visibility != DEFAULT_VISIBILITY) { 220 out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility)); 221 } 222 if (!forBackup) { 223 out.attribute(null, ATT_UID, Integer.toString(r.uid)); 224 } 225 out.endTag(null, TAG_PACKAGE); 226 } 227 out.endTag(null, TAG_RANKING); 228 } 229 230 private void updateConfig() { 231 final int N = mSignalExtractors.length; 232 for (int i = 0; i < N; i++) { 233 mSignalExtractors[i].setConfig(this); 234 } 235 mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE); 236 } 237 238 public void sort(ArrayList<NotificationRecord> notificationList) { 239 final int N = notificationList.size(); 240 // clear global sort keys 241 for (int i = N - 1; i >= 0; i--) { 242 notificationList.get(i).setGlobalSortKey(null); 243 } 244 245 // rank each record individually 246 Collections.sort(notificationList, mPreliminaryComparator); 247 248 synchronized (mProxyByGroupTmp) { 249 // record individual ranking result and nominate proxies for each group 250 for (int i = N - 1; i >= 0; i--) { 251 final NotificationRecord record = notificationList.get(i); 252 record.setAuthoritativeRank(i); 253 final String groupKey = record.getGroupKey(); 254 boolean isGroupSummary = record.getNotification().isGroupSummary(); 255 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 256 mProxyByGroupTmp.put(groupKey, record); 257 } 258 } 259 // assign global sort key: 260 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 261 for (int i = 0; i < N; i++) { 262 final NotificationRecord record = notificationList.get(i); 263 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 264 String groupSortKey = record.getNotification().getSortKey(); 265 266 // We need to make sure the developer provided group sort key (gsk) is handled 267 // correctly: 268 // gsk="" < gsk=non-null-string < gsk=null 269 // 270 // We enforce this by using different prefixes for these three cases. 271 String groupSortKeyPortion; 272 if (groupSortKey == null) { 273 groupSortKeyPortion = "nsk"; 274 } else if (groupSortKey.equals("")) { 275 groupSortKeyPortion = "esk"; 276 } else { 277 groupSortKeyPortion = "gsk=" + groupSortKey; 278 } 279 280 boolean isGroupSummary = record.getNotification().isGroupSummary(); 281 record.setGlobalSortKey( 282 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 283 record.isRecentlyIntrusive() ? '0' : '1', 284 groupProxy.getAuthoritativeRank(), 285 isGroupSummary ? '0' : '1', 286 groupSortKeyPortion, 287 record.getAuthoritativeRank())); 288 } 289 mProxyByGroupTmp.clear(); 290 } 291 292 // Do a second ranking pass, using group proxies 293 Collections.sort(notificationList, mFinalComparator); 294 } 295 296 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 297 return Collections.binarySearch(notificationList, target, mFinalComparator); 298 } 299 300 private static int safeInt(XmlPullParser parser, String att, int defValue) { 301 final String val = parser.getAttributeValue(null, att); 302 return tryParseInt(val, defValue); 303 } 304 305 private static int tryParseInt(String value, int defValue) { 306 if (TextUtils.isEmpty(value)) return defValue; 307 try { 308 return Integer.valueOf(value); 309 } catch (NumberFormatException e) { 310 return defValue; 311 } 312 } 313 314 private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) { 315 final String val = parser.getAttributeValue(null, att); 316 return tryParseBool(val, defValue); 317 } 318 319 private static boolean tryParseBool(String value, boolean defValue) { 320 if (TextUtils.isEmpty(value)) return defValue; 321 return Boolean.valueOf(value); 322 } 323 324 @Override 325 public int getPackagePriority(String packageName, int uid) { 326 final Record r = mRecords.get(recordKey(packageName, uid)); 327 return r != null ? r.priority : DEFAULT_PRIORITY; 328 } 329 330 @Override 331 public void setPackagePriority(String packageName, int uid, int priority) { 332 if (priority == getPackagePriority(packageName, uid)) { 333 return; 334 } 335 getOrCreateRecord(packageName, uid).priority = priority; 336 removeDefaultRecords(); 337 updateConfig(); 338 } 339 340 @Override 341 public int getPackageVisibilityOverride(String packageName, int uid) { 342 final Record r = mRecords.get(recordKey(packageName, uid)); 343 return r != null ? r.visibility : DEFAULT_VISIBILITY; 344 } 345 346 @Override 347 public void setPackageVisibilityOverride(String packageName, int uid, int visibility) { 348 if (visibility == getPackageVisibilityOverride(packageName, uid)) { 349 return; 350 } 351 getOrCreateRecord(packageName, uid).visibility = visibility; 352 removeDefaultRecords(); 353 updateConfig(); 354 } 355 356 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 357 if (filter == null) { 358 final int N = mSignalExtractors.length; 359 pw.print(prefix); 360 pw.print("mSignalExtractors.length = "); 361 pw.println(N); 362 for (int i = 0; i < N; i++) { 363 pw.print(prefix); 364 pw.print(" "); 365 pw.println(mSignalExtractors[i]); 366 } 367 } 368 if (filter == null) { 369 pw.print(prefix); 370 pw.println("per-package config:"); 371 } 372 dumpRecords(pw, prefix, filter, mRecords); 373 dumpRecords(pw, prefix, filter, mRestoredWithoutUids); 374 } 375 376 private static void dumpRecords(PrintWriter pw, String prefix, 377 NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) { 378 final int N = records.size(); 379 for (int i = 0; i < N; i++) { 380 final Record r = records.valueAt(i); 381 if (filter == null || filter.matches(r.pkg)) { 382 pw.print(prefix); 383 pw.print(" "); 384 pw.print(r.pkg); 385 pw.print(" ("); 386 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid)); 387 pw.print(')'); 388 if (r.priority != DEFAULT_PRIORITY) { 389 pw.print(" priority="); 390 pw.print(Notification.priorityToString(r.priority)); 391 } 392 if (r.visibility != DEFAULT_VISIBILITY) { 393 pw.print(" visibility="); 394 pw.print(Notification.visibilityToString(r.visibility)); 395 } 396 pw.println(); 397 } 398 } 399 } 400 401 public void onPackagesChanged(boolean queryReplace, String[] pkgList) { 402 if (queryReplace || pkgList == null || pkgList.length == 0 403 || mRestoredWithoutUids.isEmpty()) { 404 return; // nothing to do 405 } 406 final PackageManager pm = mContext.getPackageManager(); 407 boolean updated = false; 408 for (String pkg : pkgList) { 409 final Record r = mRestoredWithoutUids.get(pkg); 410 if (r != null) { 411 try { 412 //TODO: http://b/22388012 413 r.uid = pm.getPackageUid(r.pkg, UserHandle.USER_SYSTEM); 414 mRestoredWithoutUids.remove(pkg); 415 mRecords.put(recordKey(r.pkg, r.uid), r); 416 updated = true; 417 } catch (NameNotFoundException e) { 418 // noop 419 } 420 } 421 } 422 if (updated) { 423 updateConfig(); 424 } 425 } 426 427 private static class Record { 428 static int UNKNOWN_UID = UserHandle.USER_NULL; 429 430 String pkg; 431 int uid = UNKNOWN_UID; 432 int priority = DEFAULT_PRIORITY; 433 int visibility = DEFAULT_VISIBILITY; 434 } 435 436} 437