• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.pm.PackageManager;
21 import android.content.pm.PackageManager.NameNotFoundException;
22 import android.os.UserHandle;
23 import android.service.notification.NotificationListenerService.Ranking;
24 import android.text.TextUtils;
25 import android.util.ArrayMap;
26 import android.util.Slog;
27 
28 import com.android.server.notification.NotificationManagerService.DumpFilter;
29 
30 import org.json.JSONArray;
31 import org.json.JSONException;
32 import org.json.JSONObject;
33 import org.xmlpull.v1.XmlPullParser;
34 import org.xmlpull.v1.XmlPullParserException;
35 import org.xmlpull.v1.XmlSerializer;
36 
37 import java.io.IOException;
38 import java.io.PrintWriter;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.Map;
42 import java.util.Map.Entry;
43 
44 public class RankingHelper implements RankingConfig {
45     private static final String TAG = "RankingHelper";
46 
47     private static final int XML_VERSION = 1;
48 
49     private static final String TAG_RANKING = "ranking";
50     private static final String TAG_PACKAGE = "package";
51     private static final String ATT_VERSION = "version";
52 
53     private static final String ATT_NAME = "name";
54     private static final String ATT_UID = "uid";
55     private static final String ATT_PRIORITY = "priority";
56     private static final String ATT_VISIBILITY = "visibility";
57     private static final String ATT_IMPORTANCE = "importance";
58     private static final String ATT_TOPIC_ID = "id";
59     private static final String ATT_TOPIC_LABEL = "label";
60 
61     private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
62     private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE;
63     private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED;
64 
65     private final NotificationSignalExtractor[] mSignalExtractors;
66     private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
67     private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
68 
69     private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
70     private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
71     private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
72 
73     private final Context mContext;
74     private final RankingHandler mRankingHandler;
75 
RankingHelper(Context context, RankingHandler rankingHandler, NotificationUsageStats usageStats, String[] extractorNames)76     public RankingHelper(Context context, RankingHandler rankingHandler,
77             NotificationUsageStats usageStats, String[] extractorNames) {
78         mContext = context;
79         mRankingHandler = rankingHandler;
80 
81         final int N = extractorNames.length;
82         mSignalExtractors = new NotificationSignalExtractor[N];
83         for (int i = 0; i < N; i++) {
84             try {
85                 Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
86                 NotificationSignalExtractor extractor =
87                         (NotificationSignalExtractor) extractorClass.newInstance();
88                 extractor.initialize(mContext, usageStats);
89                 extractor.setConfig(this);
90                 mSignalExtractors[i] = extractor;
91             } catch (ClassNotFoundException e) {
92                 Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
93             } catch (InstantiationException e) {
94                 Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
95             } catch (IllegalAccessException e) {
96                 Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
97             }
98         }
99     }
100 
101     @SuppressWarnings("unchecked")
findExtractor(Class<T> extractorClass)102     public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
103         final int N = mSignalExtractors.length;
104         for (int i = 0; i < N; i++) {
105             final NotificationSignalExtractor extractor = mSignalExtractors[i];
106             if (extractorClass.equals(extractor.getClass())) {
107                 return (T) extractor;
108             }
109         }
110         return null;
111     }
112 
extractSignals(NotificationRecord r)113     public void extractSignals(NotificationRecord r) {
114         final int N = mSignalExtractors.length;
115         for (int i = 0; i < N; i++) {
116             NotificationSignalExtractor extractor = mSignalExtractors[i];
117             try {
118                 RankingReconsideration recon = extractor.process(r);
119                 if (recon != null) {
120                     mRankingHandler.requestReconsideration(recon);
121                 }
122             } catch (Throwable t) {
123                 Slog.w(TAG, "NotificationSignalExtractor failed.", t);
124             }
125         }
126     }
127 
readXml(XmlPullParser parser, boolean forRestore)128     public void readXml(XmlPullParser parser, boolean forRestore)
129             throws XmlPullParserException, IOException {
130         final PackageManager pm = mContext.getPackageManager();
131         int type = parser.getEventType();
132         if (type != XmlPullParser.START_TAG) return;
133         String tag = parser.getName();
134         if (!TAG_RANKING.equals(tag)) return;
135         mRecords.clear();
136         mRestoredWithoutUids.clear();
137         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
138             tag = parser.getName();
139             if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
140                 return;
141             }
142             if (type == XmlPullParser.START_TAG) {
143                 if (TAG_PACKAGE.equals(tag)) {
144                     int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
145                     String name = parser.getAttributeValue(null, ATT_NAME);
146 
147                     if (!TextUtils.isEmpty(name)) {
148                         if (forRestore) {
149                             try {
150                                 //TODO: http://b/22388012
151                                 uid = pm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
152                             } catch (NameNotFoundException e) {
153                                 // noop
154                             }
155                         }
156                         Record r = null;
157                         if (uid == Record.UNKNOWN_UID) {
158                             r = mRestoredWithoutUids.get(name);
159                             if (r == null) {
160                                 r = new Record();
161                                 mRestoredWithoutUids.put(name, r);
162                             }
163                         } else {
164                             r = getOrCreateRecord(name, uid);
165                         }
166                         r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
167                         r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
168                         r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
169                     }
170                 }
171             }
172         }
173         throw new IllegalStateException("Failed to reach END_DOCUMENT");
174     }
175 
recordKey(String pkg, int uid)176     private static String recordKey(String pkg, int uid) {
177         return pkg + "|" + uid;
178     }
179 
getOrCreateRecord(String pkg, int uid)180     private Record getOrCreateRecord(String pkg, int uid) {
181         final String key = recordKey(pkg, uid);
182         Record r = mRecords.get(key);
183         if (r == null) {
184             r = new Record();
185             r.pkg = pkg;
186             r.uid = uid;
187             mRecords.put(key, r);
188         }
189         return r;
190     }
191 
writeXml(XmlSerializer out, boolean forBackup)192     public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
193         out.startTag(null, TAG_RANKING);
194         out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
195 
196         final int N = mRecords.size();
197         for (int i = 0; i < N; i++) {
198             final Record r = mRecords.valueAt(i);
199             //TODO: http://b/22388012
200             if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
201                 continue;
202             }
203             final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
204                     || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY;
205             if (hasNonDefaultSettings) {
206                 out.startTag(null, TAG_PACKAGE);
207                 out.attribute(null, ATT_NAME, r.pkg);
208                 if (r.importance != DEFAULT_IMPORTANCE) {
209                     out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
210                 }
211                 if (r.priority != DEFAULT_PRIORITY) {
212                     out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
213                 }
214                 if (r.visibility != DEFAULT_VISIBILITY) {
215                     out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
216                 }
217 
218                 if (!forBackup) {
219                     out.attribute(null, ATT_UID, Integer.toString(r.uid));
220                 }
221 
222                 out.endTag(null, TAG_PACKAGE);
223             }
224         }
225         out.endTag(null, TAG_RANKING);
226     }
227 
updateConfig()228     private void updateConfig() {
229         final int N = mSignalExtractors.length;
230         for (int i = 0; i < N; i++) {
231             mSignalExtractors[i].setConfig(this);
232         }
233         mRankingHandler.requestSort();
234     }
235 
sort(ArrayList<NotificationRecord> notificationList)236     public void sort(ArrayList<NotificationRecord> notificationList) {
237         final int N = notificationList.size();
238         // clear global sort keys
239         for (int i = N - 1; i >= 0; i--) {
240             notificationList.get(i).setGlobalSortKey(null);
241         }
242 
243         // rank each record individually
244         Collections.sort(notificationList, mPreliminaryComparator);
245 
246         synchronized (mProxyByGroupTmp) {
247             // record individual ranking result and nominate proxies for each group
248             for (int i = N - 1; i >= 0; i--) {
249                 final NotificationRecord record = notificationList.get(i);
250                 record.setAuthoritativeRank(i);
251                 final String groupKey = record.getGroupKey();
252                 boolean isGroupSummary = record.getNotification().isGroupSummary();
253                 if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
254                     mProxyByGroupTmp.put(groupKey, record);
255                 }
256             }
257             // assign global sort key:
258             //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
259             for (int i = 0; i < N; i++) {
260                 final NotificationRecord record = notificationList.get(i);
261                 NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
262                 String groupSortKey = record.getNotification().getSortKey();
263 
264                 // We need to make sure the developer provided group sort key (gsk) is handled
265                 // correctly:
266                 //   gsk="" < gsk=non-null-string < gsk=null
267                 //
268                 // We enforce this by using different prefixes for these three cases.
269                 String groupSortKeyPortion;
270                 if (groupSortKey == null) {
271                     groupSortKeyPortion = "nsk";
272                 } else if (groupSortKey.equals("")) {
273                     groupSortKeyPortion = "esk";
274                 } else {
275                     groupSortKeyPortion = "gsk=" + groupSortKey;
276                 }
277 
278                 boolean isGroupSummary = record.getNotification().isGroupSummary();
279                 record.setGlobalSortKey(
280                         String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
281                         record.isRecentlyIntrusive() ? '0' : '1',
282                         groupProxy.getAuthoritativeRank(),
283                         isGroupSummary ? '0' : '1',
284                         groupSortKeyPortion,
285                         record.getAuthoritativeRank()));
286             }
287             mProxyByGroupTmp.clear();
288         }
289 
290         // Do a second ranking pass, using group proxies
291         Collections.sort(notificationList, mFinalComparator);
292     }
293 
indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target)294     public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
295         return Collections.binarySearch(notificationList, target, mFinalComparator);
296     }
297 
safeInt(XmlPullParser parser, String att, int defValue)298     private static int safeInt(XmlPullParser parser, String att, int defValue) {
299         final String val = parser.getAttributeValue(null, att);
300         return tryParseInt(val, defValue);
301     }
302 
tryParseInt(String value, int defValue)303     private static int tryParseInt(String value, int defValue) {
304         if (TextUtils.isEmpty(value)) return defValue;
305         try {
306             return Integer.parseInt(value);
307         } catch (NumberFormatException e) {
308             return defValue;
309         }
310     }
311 
tryParseBool(String value, boolean defValue)312     private static boolean tryParseBool(String value, boolean defValue) {
313         if (TextUtils.isEmpty(value)) return defValue;
314         return Boolean.valueOf(value);
315     }
316 
317     /**
318      * Gets priority.
319      */
320     @Override
getPriority(String packageName, int uid)321     public int getPriority(String packageName, int uid) {
322         return getOrCreateRecord(packageName, uid).priority;
323     }
324 
325     /**
326      * Sets priority.
327      */
328     @Override
setPriority(String packageName, int uid, int priority)329     public void setPriority(String packageName, int uid, int priority) {
330         getOrCreateRecord(packageName, uid).priority = priority;
331         updateConfig();
332     }
333 
334     /**
335      * Gets visual override.
336      */
337     @Override
getVisibilityOverride(String packageName, int uid)338     public int getVisibilityOverride(String packageName, int uid) {
339         return getOrCreateRecord(packageName, uid).visibility;
340     }
341 
342     /**
343      * Sets visibility override.
344      */
345     @Override
setVisibilityOverride(String pkgName, int uid, int visibility)346     public void setVisibilityOverride(String pkgName, int uid, int visibility) {
347         getOrCreateRecord(pkgName, uid).visibility = visibility;
348         updateConfig();
349     }
350 
351     /**
352      * Gets importance.
353      */
354     @Override
getImportance(String packageName, int uid)355     public int getImportance(String packageName, int uid) {
356         return getOrCreateRecord(packageName, uid).importance;
357     }
358 
359     /**
360      * Sets importance.
361      */
362     @Override
setImportance(String pkgName, int uid, int importance)363     public void setImportance(String pkgName, int uid, int importance) {
364         getOrCreateRecord(pkgName, uid).importance = importance;
365         updateConfig();
366     }
367 
setEnabled(String packageName, int uid, boolean enabled)368     public void setEnabled(String packageName, int uid, boolean enabled) {
369         boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE;
370         if (wasEnabled == enabled) {
371             return;
372         }
373         setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE);
374     }
375 
dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter)376     public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
377         if (filter == null) {
378             final int N = mSignalExtractors.length;
379             pw.print(prefix);
380             pw.print("mSignalExtractors.length = ");
381             pw.println(N);
382             for (int i = 0; i < N; i++) {
383                 pw.print(prefix);
384                 pw.print("  ");
385                 pw.println(mSignalExtractors[i]);
386             }
387         }
388         if (filter == null) {
389             pw.print(prefix);
390             pw.println("per-package config:");
391         }
392         pw.println("Records:");
393         dumpRecords(pw, prefix, filter, mRecords);
394         pw.println("Restored without uid:");
395         dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
396     }
397 
dumpRecords(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records)398     private static void dumpRecords(PrintWriter pw, String prefix,
399             NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
400         final int N = records.size();
401         for (int i = 0; i < N; i++) {
402             final Record r = records.valueAt(i);
403             if (filter == null || filter.matches(r.pkg)) {
404                 pw.print(prefix);
405                 pw.print("  ");
406                 pw.print(r.pkg);
407                 pw.print(" (");
408                 pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
409                 pw.print(')');
410                 if (r.importance != DEFAULT_IMPORTANCE) {
411                     pw.print(" importance=");
412                     pw.print(Ranking.importanceToString(r.importance));
413                 }
414                 if (r.priority != DEFAULT_PRIORITY) {
415                     pw.print(" priority=");
416                     pw.print(Notification.priorityToString(r.priority));
417                 }
418                 if (r.visibility != DEFAULT_VISIBILITY) {
419                     pw.print(" visibility=");
420                     pw.print(Notification.visibilityToString(r.visibility));
421                 }
422                 pw.println();
423             }
424         }
425     }
426 
dumpJson(NotificationManagerService.DumpFilter filter)427     public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
428         JSONObject ranking = new JSONObject();
429         JSONArray records = new JSONArray();
430         try {
431             ranking.put("noUid", mRestoredWithoutUids.size());
432         } catch (JSONException e) {
433            // pass
434         }
435         final int N = mRecords.size();
436         for (int i = 0; i < N; i++) {
437             final Record r = mRecords.valueAt(i);
438             if (filter == null || filter.matches(r.pkg)) {
439                 JSONObject record = new JSONObject();
440                 try {
441                     record.put("userId", UserHandle.getUserId(r.uid));
442                     record.put("packageName", r.pkg);
443                     if (r.importance != DEFAULT_IMPORTANCE) {
444                         record.put("importance", Ranking.importanceToString(r.importance));
445                     }
446                     if (r.priority != DEFAULT_PRIORITY) {
447                         record.put("priority", Notification.priorityToString(r.priority));
448                     }
449                     if (r.visibility != DEFAULT_VISIBILITY) {
450                         record.put("visibility", Notification.visibilityToString(r.visibility));
451                     }
452                 } catch (JSONException e) {
453                    // pass
454                 }
455                 records.put(record);
456             }
457         }
458         try {
459             ranking.put("records", records);
460         } catch (JSONException e) {
461             // pass
462         }
463         return ranking;
464     }
465 
466     /**
467      * Dump only the ban information as structured JSON for the stats collector.
468      *
469      * This is intentionally redundant with {#link dumpJson} because the old
470      * scraper will expect this format.
471      *
472      * @param filter
473      * @return
474      */
dumpBansJson(NotificationManagerService.DumpFilter filter)475     public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
476         JSONArray bans = new JSONArray();
477         Map<Integer, String> packageBans = getPackageBans();
478         for(Entry<Integer, String> ban : packageBans.entrySet()) {
479             final int userId = UserHandle.getUserId(ban.getKey());
480             final String packageName = ban.getValue();
481             if (filter == null || filter.matches(packageName)) {
482                 JSONObject banJson = new JSONObject();
483                 try {
484                     banJson.put("userId", userId);
485                     banJson.put("packageName", packageName);
486                 } catch (JSONException e) {
487                     e.printStackTrace();
488                 }
489                 bans.put(banJson);
490             }
491         }
492         return bans;
493     }
494 
getPackageBans()495     public Map<Integer, String> getPackageBans() {
496         final int N = mRecords.size();
497         ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
498         for (int i = 0; i < N; i++) {
499             final Record r = mRecords.valueAt(i);
500             if (r.importance == Ranking.IMPORTANCE_NONE) {
501                 packageBans.put(r.uid, r.pkg);
502             }
503         }
504         return packageBans;
505     }
506 
onPackagesChanged(boolean removingPackage, String[] pkgList)507     public void onPackagesChanged(boolean removingPackage, String[] pkgList) {
508         if (!removingPackage || pkgList == null || pkgList.length == 0
509                 || mRestoredWithoutUids.isEmpty()) {
510             return; // nothing to do
511         }
512         final PackageManager pm = mContext.getPackageManager();
513         boolean updated = false;
514         for (String pkg : pkgList) {
515             final Record r = mRestoredWithoutUids.get(pkg);
516             if (r != null) {
517                 try {
518                     //TODO: http://b/22388012
519                     r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
520                     mRestoredWithoutUids.remove(pkg);
521                     mRecords.put(recordKey(r.pkg, r.uid), r);
522                     updated = true;
523                 } catch (NameNotFoundException e) {
524                     // noop
525                 }
526             }
527         }
528         if (updated) {
529             updateConfig();
530         }
531     }
532 
533     private static class Record {
534         static int UNKNOWN_UID = UserHandle.USER_NULL;
535 
536         String pkg;
537         int uid = UNKNOWN_UID;
538         int importance = DEFAULT_IMPORTANCE;
539         int priority = DEFAULT_PRIORITY;
540         int visibility = DEFAULT_VISIBILITY;
541    }
542 }
543