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 */ 16 package com.android.server.notification; 17 18 import android.app.Notification; 19 import android.content.Context; 20 import android.os.Handler; 21 import android.os.Message; 22 import android.os.UserHandle; 23 import android.service.notification.NotificationListenerService; 24 import android.text.TextUtils; 25 import android.util.ArrayMap; 26 import android.util.ArraySet; 27 import android.util.Log; 28 import android.util.Slog; 29 import android.util.SparseIntArray; 30 import org.xmlpull.v1.XmlPullParser; 31 import org.xmlpull.v1.XmlPullParserException; 32 import org.xmlpull.v1.XmlSerializer; 33 34 import java.io.IOException; 35 import java.io.PrintWriter; 36 import java.util.ArrayList; 37 import java.util.Collections; 38 import java.util.Set; 39 import java.util.concurrent.TimeUnit; 40 41 public 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 // rank each record individually 231 Collections.sort(notificationList, mPreliminaryComparator); 232 233 synchronized (mProxyByGroupTmp) { 234 // record individual ranking result and nominate proxies for each group 235 for (int i = N - 1; i >= 0; i--) { 236 final NotificationRecord record = notificationList.get(i); 237 record.setAuthoritativeRank(i); 238 final String groupKey = record.getGroupKey(); 239 boolean isGroupSummary = record.getNotification().isGroupSummary(); 240 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) { 241 mProxyByGroupTmp.put(groupKey, record); 242 } 243 } 244 // assign global sort key: 245 // is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank 246 for (int i = 0; i < N; i++) { 247 final NotificationRecord record = notificationList.get(i); 248 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey()); 249 String groupSortKey = record.getNotification().getSortKey(); 250 251 // We need to make sure the developer provided group sort key (gsk) is handled 252 // correctly: 253 // gsk="" < gsk=non-null-string < gsk=null 254 // 255 // We enforce this by using different prefixes for these three cases. 256 String groupSortKeyPortion; 257 if (groupSortKey == null) { 258 groupSortKeyPortion = "nsk"; 259 } else if (groupSortKey.equals("")) { 260 groupSortKeyPortion = "esk"; 261 } else { 262 groupSortKeyPortion = "gsk=" + groupSortKey; 263 } 264 265 boolean isGroupSummary = record.getNotification().isGroupSummary(); 266 record.setGlobalSortKey( 267 String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x", 268 record.isRecentlyIntrusive() ? '0' : '1', 269 groupProxy.getAuthoritativeRank(), 270 isGroupSummary ? '0' : '1', 271 groupSortKeyPortion, 272 record.getAuthoritativeRank())); 273 } 274 mProxyByGroupTmp.clear(); 275 } 276 277 // Do a second ranking pass, using group proxies 278 Collections.sort(notificationList, mFinalComparator); 279 } 280 281 public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) { 282 return Collections.binarySearch(notificationList, target, mFinalComparator); 283 } 284 285 private static int safeInt(XmlPullParser parser, String att, int defValue) { 286 final String val = parser.getAttributeValue(null, att); 287 return tryParseInt(val, defValue); 288 } 289 290 private static int tryParseInt(String value, int defValue) { 291 if (TextUtils.isEmpty(value)) return defValue; 292 try { 293 return Integer.valueOf(value); 294 } catch (NumberFormatException e) { 295 return defValue; 296 } 297 } 298 299 @Override 300 public int getPackagePriority(String packageName, int uid) { 301 int priority = Notification.PRIORITY_DEFAULT; 302 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 303 if (priorityByUid != null) { 304 priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT); 305 } 306 return 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 SparseIntArray priorityByUid = mPackagePriorities.get(packageName); 315 if (priorityByUid == null) { 316 priorityByUid = new SparseIntArray(); 317 mPackagePriorities.put(packageName, priorityByUid); 318 } 319 priorityByUid.put(uid, priority); 320 updateConfig(); 321 } 322 323 @Override 324 public int getPackageVisibilityOverride(String packageName, int uid) { 325 int visibility = NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE; 326 SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName); 327 if (visibilityByUid != null) { 328 visibility = visibilityByUid.get(uid, 329 NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE); 330 } 331 return visibility; 332 } 333 334 @Override 335 public void setPackageVisibilityOverride(String packageName, int uid, int visibility) { 336 if (visibility == getPackageVisibilityOverride(packageName, uid)) { 337 return; 338 } 339 SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName); 340 if (visibilityByUid == null) { 341 visibilityByUid = new SparseIntArray(); 342 mPackageVisibilities.put(packageName, visibilityByUid); 343 } 344 visibilityByUid.put(uid, visibility); 345 updateConfig(); 346 } 347 348 public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) { 349 if (filter == null) { 350 final int N = mSignalExtractors.length; 351 pw.print(prefix); 352 pw.print("mSignalExtractors.length = "); 353 pw.println(N); 354 for (int i = 0; i < N; i++) { 355 pw.print(prefix); 356 pw.print(" "); 357 pw.println(mSignalExtractors[i]); 358 } 359 } 360 final int N = mPackagePriorities.size(); 361 if (filter == null) { 362 pw.print(prefix); 363 pw.println("package priorities:"); 364 } 365 for (int i = 0; i < N; i++) { 366 String name = mPackagePriorities.keyAt(i); 367 if (filter == null || filter.matches(name)) { 368 SparseIntArray priorityByUid = mPackagePriorities.get(name); 369 final int M = priorityByUid.size(); 370 for (int j = 0; j < M; j++) { 371 int uid = priorityByUid.keyAt(j); 372 int priority = priorityByUid.get(uid); 373 pw.print(prefix); 374 pw.print(" "); 375 pw.print(name); 376 pw.print(" ("); 377 pw.print(uid); 378 pw.print(") has priority: "); 379 pw.println(priority); 380 } 381 } 382 } 383 } 384 } 385