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