RankingHelper.java revision 3ad4e3a45bbe44129b14c4d391431e44f1e04f0c
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.ArraySet; 27import android.util.Log; 28import android.util.Slog; 29import android.util.SparseIntArray; 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.Set; 39import java.util.concurrent.TimeUnit; 40 41public class RankingHelper implements RankingConfig { 42 private static final String TAG = "RankingHelper"; 43 private static final boolean DEBUG = false; 44 45 private static final int XML_VERSION = 1; 46 47 private static final String TAG_RANKING = "ranking"; 48 private static final String TAG_PACKAGE = "package"; 49 private static final String ATT_VERSION = "version"; 50 51 private static final String ATT_NAME = "name"; 52 private static final String ATT_UID = "uid"; 53 private static final String ATT_PRIORITY = "priority"; 54 private static final String ATT_VISIBILITY = "visibility"; 55 56 private final NotificationSignalExtractor[] mSignalExtractors; 57 private final NotificationComparator mPreliminaryComparator = new NotificationComparator(); 58 private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator(); 59 60 // Package name to uid, to priority. Would be better as Table<String, Int, Int> 61 private final ArrayMap<String, SparseIntArray> mPackagePriorities; 62 private final ArrayMap<String, SparseIntArray> mPackageVisibilities; 63 private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp; 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 mPackagePriorities = new ArrayMap<String, SparseIntArray>(); 72 mPackageVisibilities = new ArrayMap<String, SparseIntArray>(); 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); 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 mProxyByGroupTmp = new ArrayMap<String, NotificationRecord>(); 93 } 94 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) throws XmlPullParserException, IOException { 125 int type = parser.getEventType(); 126 if (type != XmlPullParser.START_TAG) return; 127 String tag = parser.getName(); 128 if (!TAG_RANKING.equals(tag)) return; 129 mPackagePriorities.clear(); 130 final int version = safeInt(parser, ATT_VERSION, XML_VERSION); 131 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 132 tag = parser.getName(); 133 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 134 return; 135 } 136 if (type == XmlPullParser.START_TAG) { 137 if (TAG_PACKAGE.equals(tag)) { 138 int uid = safeInt(parser, ATT_UID, UserHandle.USER_ALL); 139 int priority = safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT); 140 int vis = safeInt(parser, ATT_VISIBILITY, 141 NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); 142 String name = parser.getAttributeValue(null, ATT_NAME); 143 144 if (!TextUtils.isEmpty(name)) { 145 if (priority != Notification.PRIORITY_DEFAULT) { 146 SparseIntArray priorityByUid = mPackagePriorities.get(name); 147 if (priorityByUid == null) { 148 priorityByUid = new SparseIntArray(); 149 mPackagePriorities.put(name, priorityByUid); 150 } 151 priorityByUid.put(uid, priority); 152 } 153 if (vis != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) { 154 SparseIntArray visibilityByUid = mPackageVisibilities.get(name); 155 if (visibilityByUid == null) { 156 visibilityByUid = new SparseIntArray(); 157 mPackageVisibilities.put(name, visibilityByUid); 158 } 159 visibilityByUid.put(uid, vis); 160 } 161 } 162 } 163 } 164 } 165 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 166 } 167 168 public void writeXml(XmlSerializer out) throws IOException { 169 out.startTag(null, TAG_RANKING); 170 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 171 172 final Set<String> packageNames = new ArraySet<>(mPackagePriorities.size() 173 + mPackageVisibilities.size()); 174 packageNames.addAll(mPackagePriorities.keySet()); 175 packageNames.addAll(mPackageVisibilities.keySet()); 176 final Set<Integer> packageUids = new ArraySet<>(); 177 for (String packageName : packageNames) { 178 packageUids.clear(); 179 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 180 SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName); 181 if (priorityByUid != null) { 182 final int M = priorityByUid.size(); 183 for (int j = 0; j < M; j++) { 184 packageUids.add(priorityByUid.keyAt(j)); 185 } 186 } 187 if (visibilityByUid != null) { 188 final int M = visibilityByUid.size(); 189 for (int j = 0; j < M; j++) { 190 packageUids.add(visibilityByUid.keyAt(j)); 191 } 192 } 193 for (Integer uid : packageUids) { 194 out.startTag(null, TAG_PACKAGE); 195 out.attribute(null, ATT_NAME, packageName); 196 if (priorityByUid != null) { 197 final int priority = priorityByUid.get(uid); 198 if (priority != Notification.PRIORITY_DEFAULT) { 199 out.attribute(null, ATT_PRIORITY, Integer.toString(priority)); 200 } 201 } 202 if (visibilityByUid != null) { 203 final int visibility = visibilityByUid.get(uid); 204 if (visibility != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) { 205 out.attribute(null, ATT_VISIBILITY, Integer.toString(visibility)); 206 } 207 } 208 out.attribute(null, ATT_UID, Integer.toString(uid)); 209 out.endTag(null, TAG_PACKAGE); 210 } 211 } 212 out.endTag(null, TAG_RANKING); 213 } 214 215 private void updateConfig() { 216 final int N = mSignalExtractors.length; 217 for (int i = 0; i < N; i++) { 218 mSignalExtractors[i].setConfig(this); 219 } 220 mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE); 221 } 222 223 public void sort(ArrayList<NotificationRecord> notificationList) { 224 final int N = notificationList.size(); 225 // clear global sort keys 226 for (int i = N - 1; i >= 0; i--) { 227 notificationList.get(i).setGlobalSortKey(null); 228 } 229 230 try { 231 // rank each record individually 232 Collections.sort(notificationList, mPreliminaryComparator); 233 } catch (RuntimeException ex) { 234 // Don't crash the system server if something bad happened. 235 Log.e(TAG, "Extreme badness during notification sort", ex); 236 Log.e(TAG, "Current notification list: "); 237 for (int i = 0; i < N; i++) { 238 NotificationRecord nr = notificationList.get(i); 239 Log.e(TAG, String.format( 240 " [%d] %s (group %s, rank %d, sortkey %s)", 241 i, nr, nr.getGroupKey(), nr.getAuthoritativeRank(), 242 nr.getNotification().getSortKey())); 243 } 244 // STOPSHIP: remove once b/16626175 is found 245 throw ex; 246 } 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 @Override 315 public int getPackagePriority(String packageName, int uid) { 316 int priority = Notification.PRIORITY_DEFAULT; 317 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 318 if (priorityByUid != null) { 319 priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT); 320 } 321 return priority; 322 } 323 324 @Override 325 public void setPackagePriority(String packageName, int uid, int priority) { 326 if (priority == getPackagePriority(packageName, uid)) { 327 return; 328 } 329 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 330 if (priorityByUid == null) { 331 priorityByUid = new SparseIntArray(); 332 mPackagePriorities.put(packageName, priorityByUid); 333 } 334 priorityByUid.put(uid, priority); 335 updateConfig(); 336 } 337 338 @Override 339 public int getPackageVisibilityOverride(String packageName, int uid) { 340 int visibility = NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE; 341 SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName); 342 if (visibilityByUid != null) { 343 visibility = visibilityByUid.get(uid, 344 NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); 345 } 346 return visibility; 347 } 348 349 @Override 350 public void setPackageVisibilityOverride(String packageName, int uid, int visibility) { 351 if (visibility == getPackageVisibilityOverride(packageName, uid)) { 352 return; 353 } 354 SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName); 355 if (visibilityByUid == null) { 356 visibilityByUid = new SparseIntArray(); 357 mPackageVisibilities.put(packageName, visibilityByUid); 358 } 359 visibilityByUid.put(uid, visibility); 360 updateConfig(); 361 } 362 363 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 364 if (filter == null) { 365 final int N = mSignalExtractors.length; 366 pw.print(prefix); 367 pw.print("mSignalExtractors.length = "); 368 pw.println(N); 369 for (int i = 0; i < N; i++) { 370 pw.print(prefix); 371 pw.print(" "); 372 pw.println(mSignalExtractors[i]); 373 } 374 } 375 final int N = mPackagePriorities.size(); 376 if (filter == null) { 377 pw.print(prefix); 378 pw.println("package priorities:"); 379 } 380 for (int i = 0; i < N; i++) { 381 String name = mPackagePriorities.keyAt(i); 382 if (filter == null || filter.matches(name)) { 383 SparseIntArray priorityByUid = mPackagePriorities.get(name); 384 final int M = priorityByUid.size(); 385 for (int j = 0; j < M; j++) { 386 int uid = priorityByUid.keyAt(j); 387 int priority = priorityByUid.get(uid); 388 pw.print(prefix); 389 pw.print(" "); 390 pw.print(name); 391 pw.print(" ("); 392 pw.print(uid); 393 pw.print(") has priority: "); 394 pw.println(priority); 395 } 396 } 397 } 398 } 399} 400