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