RankingHelper.java revision 2b122f4c2e691f0319e4f9ea5873989792bb56a6
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.text.TextUtils; 24import android.util.ArrayMap; 25import android.util.Log; 26import android.util.Slog; 27import android.util.SparseIntArray; 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.Comparator; 37import java.util.concurrent.TimeUnit; 38 39public class RankingHelper implements RankingConfig { 40 private static final String TAG = "RankingHelper"; 41 private static final boolean DEBUG = false; 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 53 private final NotificationSignalExtractor[] mSignalExtractors; 54 private final NotificationComparator mPreliminaryComparator = new NotificationComparator(); 55 private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator(); 56 57 // Package name to uid, to priority. Would be better as Table<String, Int, Int> 58 private final ArrayMap<String, SparseIntArray> mPackagePriorities; 59 private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp; 60 61 private final Context mContext; 62 private final Handler mRankingHandler; 63 64 public RankingHelper(Context context, Handler rankingHandler, String[] extractorNames) { 65 mContext = context; 66 mRankingHandler = rankingHandler; 67 mPackagePriorities = new ArrayMap<String, SparseIntArray>(); 68 69 final int N = extractorNames.length; 70 mSignalExtractors = new NotificationSignalExtractor[N]; 71 for (int i = 0; i < N; i++) { 72 try { 73 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]); 74 NotificationSignalExtractor extractor = 75 (NotificationSignalExtractor) extractorClass.newInstance(); 76 extractor.initialize(mContext); 77 extractor.setConfig(this); 78 mSignalExtractors[i] = extractor; 79 } catch (ClassNotFoundException e) { 80 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e); 81 } catch (InstantiationException e) { 82 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e); 83 } catch (IllegalAccessException e) { 84 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e); 85 } 86 } 87 mProxyByGroupTmp = new ArrayMap<String, NotificationRecord>(); 88 } 89 90 public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) { 91 final int N = mSignalExtractors.length; 92 for (int i = 0; i < N; i++) { 93 final NotificationSignalExtractor extractor = mSignalExtractors[i]; 94 if (extractorClass.equals(extractor.getClass())) { 95 return (T) extractor; 96 } 97 } 98 return null; 99 } 100 101 public void extractSignals(NotificationRecord r) { 102 final int N = mSignalExtractors.length; 103 for (int i = 0; i < N; i++) { 104 NotificationSignalExtractor extractor = mSignalExtractors[i]; 105 try { 106 RankingReconsideration recon = extractor.process(r); 107 if (recon != null) { 108 Message m = Message.obtain(mRankingHandler, 109 NotificationManagerService.MESSAGE_RECONSIDER_RANKING, recon); 110 long delay = recon.getDelay(TimeUnit.MILLISECONDS); 111 mRankingHandler.sendMessageDelayed(m, delay); 112 } 113 } catch (Throwable t) { 114 Slog.w(TAG, "NotificationSignalExtractor failed.", t); 115 } 116 } 117 } 118 119 public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException { 120 int type = parser.getEventType(); 121 if (type != XmlPullParser.START_TAG) return; 122 String tag = parser.getName(); 123 if (!TAG_RANKING.equals(tag)) return; 124 mPackagePriorities.clear(); 125 final int version = safeInt(parser, ATT_VERSION, XML_VERSION); 126 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { 127 tag = parser.getName(); 128 if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) { 129 return; 130 } 131 if (type == XmlPullParser.START_TAG) { 132 if (TAG_PACKAGE.equals(tag)) { 133 int uid = safeInt(parser, ATT_UID, UserHandle.USER_ALL); 134 int priority = safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT); 135 String name = parser.getAttributeValue(null, ATT_NAME); 136 137 if (!TextUtils.isEmpty(name) && priority != Notification.PRIORITY_DEFAULT) { 138 SparseIntArray priorityByUid = mPackagePriorities.get(name); 139 if (priorityByUid == null) { 140 priorityByUid = new SparseIntArray(); 141 mPackagePriorities.put(name, priorityByUid); 142 } 143 priorityByUid.put(uid, priority); 144 } 145 } 146 } 147 } 148 throw new IllegalStateException("Failed to reach END_DOCUMENT"); 149 } 150 151 public void writeXml(XmlSerializer out) throws IOException { 152 out.startTag(null, TAG_RANKING); 153 out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION)); 154 155 final int N = mPackagePriorities.size(); 156 for (int i = 0; i < N; i ++) { 157 String name = mPackagePriorities.keyAt(i); 158 SparseIntArray priorityByUid = mPackagePriorities.get(name); 159 final int M = priorityByUid.size(); 160 for (int j = 0; j < M; j++) { 161 int uid = priorityByUid.keyAt(j); 162 int priority = priorityByUid.get(uid); 163 out.startTag(null, TAG_PACKAGE); 164 out.attribute(null, ATT_NAME, name); 165 out.attribute(null, ATT_UID, Integer.toString(uid)); 166 out.attribute(null, ATT_PRIORITY, Integer.toString(priority)); 167 out.endTag(null, TAG_PACKAGE); 168 } 169 } 170 out.endTag(null, TAG_RANKING); 171 } 172 173 private void updateConfig() { 174 final int N = mSignalExtractors.length; 175 for (int i = 0; i < N; i++) { 176 mSignalExtractors[i].setConfig(this); 177 } 178 mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE); 179 } 180 181 public void sort(ArrayList<NotificationRecord> notificationList) { 182 final int N = notificationList.size(); 183 // clear global sort keys 184 for (int i = N - 1; i >= 0; i--) { 185 notificationList.get(i).setGlobalSortKey(null); 186 } 187 188 try { 189 // rank each record individually 190 Collections.sort(notificationList, mPreliminaryComparator); 191 } catch (RuntimeException ex) { 192 // Don't crash the system server if something bad happened. 193 Log.e(TAG, "Extreme badness during notification sort", ex); 194 Log.e(TAG, "Current notification list: "); 195 for (int i = 0; i < N; i++) { 196 NotificationRecord nr = notificationList.get(i); 197 Log.e(TAG, String.format( 198 " [%d] %s (group %s, rank %d, sortkey %s)", 199 i, nr, nr.getGroupKey(), nr.getAuthoritativeRank(), 200 nr.getNotification().getSortKey())); 201 } 202 // STOPSHIP: remove once b/16626175 is found 203 throw ex; 204 } 205 206 synchronized (mProxyByGroupTmp) { 207 // record individual ranking result and nominate proxies for each group 208 for (int i = N - 1; i >= 0; i--) { 209 final NotificationRecord record = notificationList.get(i); 210 record.setAuthoritativeRank(i); 211 final String groupKey = record.getGroupKey(); 212 boolean isGroupSummary = record.getNotification().isGroupSummary(); 213 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 214 mProxyByGroupTmp.put(groupKey, record); 215 } 216 } 217 // assign global sort key: 218 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 219 for (int i = 0; i < N; i++) { 220 final NotificationRecord record = notificationList.get(i); 221 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 222 String groupSortKey = record.getNotification().getSortKey(); 223 224 // We need to make sure the developer provided group sort key (gsk) is handled 225 // correctly: 226 // gsk="" < gsk=non-null-string < gsk=null 227 // 228 // We enforce this by using different prefixes for these three cases. 229 String groupSortKeyPortion; 230 if (groupSortKey == null) { 231 groupSortKeyPortion = "nsk"; 232 } else if (groupSortKey.equals("")) { 233 groupSortKeyPortion = "esk"; 234 } else { 235 groupSortKeyPortion = "gsk=" + groupSortKey; 236 } 237 238 boolean isGroupSummary = record.getNotification().isGroupSummary(); 239 record.setGlobalSortKey( 240 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 241 record.isRecentlyIntrusive() ? '0' : '1', 242 groupProxy.getAuthoritativeRank(), 243 isGroupSummary ? '0' : '1', 244 groupSortKeyPortion, 245 record.getAuthoritativeRank())); 246 } 247 mProxyByGroupTmp.clear(); 248 } 249 250 // Do a second ranking pass, using group proxies 251 Collections.sort(notificationList, mFinalComparator); 252 } 253 254 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 255 return Collections.binarySearch(notificationList, target, mFinalComparator); 256 } 257 258 private static int safeInt(XmlPullParser parser, String att, int defValue) { 259 final String val = parser.getAttributeValue(null, att); 260 return tryParseInt(val, defValue); 261 } 262 263 private static int tryParseInt(String value, int defValue) { 264 if (TextUtils.isEmpty(value)) return defValue; 265 try { 266 return Integer.valueOf(value); 267 } catch (NumberFormatException e) { 268 return defValue; 269 } 270 } 271 272 @Override 273 public int getPackagePriority(String packageName, int uid) { 274 int priority = Notification.PRIORITY_DEFAULT; 275 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 276 if (priorityByUid != null) { 277 priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT); 278 } 279 return priority; 280 } 281 282 @Override 283 public void setPackagePriority(String packageName, int uid, int priority) { 284 if (priority == getPackagePriority(packageName, uid)) { 285 return; 286 } 287 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 288 if (priorityByUid == null) { 289 priorityByUid = new SparseIntArray(); 290 mPackagePriorities.put(packageName, priorityByUid); 291 } 292 priorityByUid.put(uid, priority); 293 updateConfig(); 294 } 295 296 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 297 if (filter == null) { 298 final int N = mSignalExtractors.length; 299 pw.print(prefix); 300 pw.print("mSignalExtractors.length = "); 301 pw.println(N); 302 for (int i = 0; i < N; i++) { 303 pw.print(prefix); 304 pw.print(" "); 305 pw.println(mSignalExtractors[i]); 306 } 307 } 308 final int N = mPackagePriorities.size(); 309 if (filter == null) { 310 pw.print(prefix); 311 pw.println("package priorities:"); 312 } 313 for (int i = 0; i < N; i++) { 314 String name = mPackagePriorities.keyAt(i); 315 if (filter == null || filter.matches(name)) { 316 SparseIntArray priorityByUid = mPackagePriorities.get(name); 317 final int M = priorityByUid.size(); 318 for (int j = 0; j < M; j++) { 319 int uid = priorityByUid.keyAt(j); 320 int priority = priorityByUid.get(uid); 321 pw.print(prefix); 322 pw.print(" "); 323 pw.print(name); 324 pw.print(" ("); 325 pw.print(uid); 326 pw.print(") has priority: "); 327 pw.println(priority); 328 } 329 } 330 } 331 } 332} 333