RankingHelper.java revision e06b4d1d9f718b9fe02980fea794a36831a16db2
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.content.pm.PackageManager;
21import android.content.pm.PackageManager.NameNotFoundException;
22import android.os.UserHandle;
23import android.service.notification.NotificationListenerService.Ranking;
24import android.text.TextUtils;
25import android.util.ArrayMap;
26import android.util.Slog;
27
28import com.android.internal.R;
29
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.List;
39import java.util.Map;
40
41public class RankingHelper implements RankingConfig {
42    private static final String TAG = "RankingHelper";
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    private static final String TAG_TOPIC = "topic";
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    private static final String ATT_IMPORTANCE = "importance";
56    private static final String ATT_TOPIC_ID = "id";
57    private static final String ATT_TOPIC_LABEL = "label";
58
59    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
60    private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE;
61    private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED;
62
63    private final NotificationSignalExtractor[] mSignalExtractors;
64    private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
65    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
66
67    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
68    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
69    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
70
71    private final Context mContext;
72    private final RankingHandler mRankingHandler;
73
74    public RankingHelper(Context context, RankingHandler rankingHandler,
75            NotificationUsageStats usageStats, String[] extractorNames) {
76        mContext = context;
77        mRankingHandler = rankingHandler;
78
79        final int N = extractorNames.length;
80        mSignalExtractors = new NotificationSignalExtractor[N];
81        for (int i = 0; i < N; i++) {
82            try {
83                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
84                NotificationSignalExtractor extractor =
85                        (NotificationSignalExtractor) extractorClass.newInstance();
86                extractor.initialize(mContext, usageStats);
87                extractor.setConfig(this);
88                mSignalExtractors[i] = extractor;
89            } catch (ClassNotFoundException e) {
90                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
91            } catch (InstantiationException e) {
92                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
93            } catch (IllegalAccessException e) {
94                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
95            }
96        }
97    }
98
99    @SuppressWarnings("unchecked")
100    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
101        final int N = mSignalExtractors.length;
102        for (int i = 0; i < N; i++) {
103            final NotificationSignalExtractor extractor = mSignalExtractors[i];
104            if (extractorClass.equals(extractor.getClass())) {
105                return (T) extractor;
106            }
107        }
108        return null;
109    }
110
111    public void extractSignals(NotificationRecord r) {
112        final int N = mSignalExtractors.length;
113        for (int i = 0; i < N; i++) {
114            NotificationSignalExtractor extractor = mSignalExtractors[i];
115            try {
116                RankingReconsideration recon = extractor.process(r);
117                if (recon != null) {
118                    mRankingHandler.requestReconsideration(recon);
119                }
120            } catch (Throwable t) {
121                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
122            }
123        }
124    }
125
126    public void readXml(XmlPullParser parser, boolean forRestore)
127            throws XmlPullParserException, IOException {
128        final PackageManager pm = mContext.getPackageManager();
129        int type = parser.getEventType();
130        if (type != XmlPullParser.START_TAG) return;
131        String tag = parser.getName();
132        if (!TAG_RANKING.equals(tag)) return;
133        mRecords.clear();
134        mRestoredWithoutUids.clear();
135        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
136            tag = parser.getName();
137            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
138                return;
139            }
140            if (type == XmlPullParser.START_TAG) {
141                if (TAG_PACKAGE.equals(tag)) {
142                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
143                    int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
144                    int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
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
168                        // Migrate package level settings to the default topic.
169                        // Might be overwritten by parseTopics.
170                        Topic defaultTopic = r.topics.get(Notification.TOPIC_DEFAULT);
171                        defaultTopic.priority = priority;
172                        defaultTopic.visibility = vis;
173
174                        parseTopics(r, parser);
175                    }
176                }
177            }
178        }
179        throw new IllegalStateException("Failed to reach END_DOCUMENT");
180    }
181
182    public void parseTopics(Record r, XmlPullParser parser)
183            throws XmlPullParserException, IOException {
184        final int innerDepth = parser.getDepth();
185        int type;
186        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
187                && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
188            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
189                continue;
190            }
191
192            String tagName = parser.getName();
193            if (TAG_TOPIC.equals(tagName)) {
194                int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
195                int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
196                int importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
197                String id = parser.getAttributeValue(null, ATT_TOPIC_ID);
198                CharSequence label = parser.getAttributeValue(null, ATT_TOPIC_LABEL);
199
200                if (!TextUtils.isEmpty(id)) {
201                    Topic topic = new Topic(new Notification.Topic(id, label));
202
203                    if (priority != DEFAULT_PRIORITY) {
204                        topic.priority = priority;
205                    }
206                    if (vis != DEFAULT_VISIBILITY) {
207                        topic.visibility = vis;
208                    }
209                    if (importance != DEFAULT_IMPORTANCE) {
210                        topic.importance = importance;
211                    }
212                    r.topics.put(id, topic);
213                }
214            }
215        }
216    }
217
218    private static String recordKey(String pkg, int uid) {
219        return pkg + "|" + uid;
220    }
221
222    private Record getOrCreateRecord(String pkg, int uid) {
223        final String key = recordKey(pkg, uid);
224        Record r = mRecords.get(key);
225        if (r == null) {
226            r = new Record();
227            r.pkg = pkg;
228            r.uid = uid;
229            r.topics.put(Notification.TOPIC_DEFAULT, new Topic(createDefaultTopic()));
230            mRecords.put(key, r);
231        }
232        return r;
233    }
234
235    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
236        out.startTag(null, TAG_RANKING);
237        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
238
239        final int N = mRecords.size();
240        for (int i = 0; i < N; i++) {
241            final Record r = mRecords.valueAt(i);
242            //TODO: http://b/22388012
243            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
244                continue;
245            }
246            out.startTag(null, TAG_PACKAGE);
247            out.attribute(null, ATT_NAME, r.pkg);
248            out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
249
250            if (!forBackup) {
251                out.attribute(null, ATT_UID, Integer.toString(r.uid));
252            }
253
254            writeTopicsXml(out, r);
255            out.endTag(null, TAG_PACKAGE);
256        }
257        out.endTag(null, TAG_RANKING);
258    }
259
260    public void writeTopicsXml(XmlSerializer out, Record r) throws IOException {
261        for (Topic t : r.topics.values()) {
262            out.startTag(null, TAG_TOPIC);
263            out.attribute(null, ATT_TOPIC_ID, t.topic.getId());
264            out.attribute(null, ATT_TOPIC_LABEL, t.topic.getLabel().toString());
265            if (t.priority != DEFAULT_PRIORITY) {
266                out.attribute(null, ATT_PRIORITY, Integer.toString(t.priority));
267            }
268            if (t.visibility != DEFAULT_VISIBILITY) {
269                out.attribute(null, ATT_VISIBILITY, Integer.toString(t.visibility));
270            }
271            if (t.importance != DEFAULT_IMPORTANCE) {
272                out.attribute(null, ATT_IMPORTANCE, Integer.toString(t.importance));
273            }
274            out.endTag(null, TAG_TOPIC);
275        }
276    }
277
278    private void updateConfig() {
279        final int N = mSignalExtractors.length;
280        for (int i = 0; i < N; i++) {
281            mSignalExtractors[i].setConfig(this);
282        }
283        mRankingHandler.requestSort();
284    }
285
286    public void sort(ArrayList<NotificationRecord> notificationList) {
287        final int N = notificationList.size();
288        // clear global sort keys
289        for (int i = N - 1; i >= 0; i--) {
290            notificationList.get(i).setGlobalSortKey(null);
291        }
292
293        // rank each record individually
294        Collections.sort(notificationList, mPreliminaryComparator);
295
296        synchronized (mProxyByGroupTmp) {
297            // record individual ranking result and nominate proxies for each group
298            for (int i = N - 1; i >= 0; i--) {
299                final NotificationRecord record = notificationList.get(i);
300                record.setAuthoritativeRank(i);
301                final String groupKey = record.getGroupKey();
302                boolean isGroupSummary = record.getNotification().isGroupSummary();
303                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
304                    mProxyByGroupTmp.put(groupKey, record);
305                }
306            }
307            // assign global sort key:
308            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
309            for (int i = 0; i < N; i++) {
310                final NotificationRecord record = notificationList.get(i);
311                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
312                String groupSortKey = record.getNotification().getSortKey();
313
314                // We need to make sure the developer provided group sort key (gsk) is handled
315                // correctly:
316                //   gsk="" < gsk=non-null-string < gsk=null
317                //
318                // We enforce this by using different prefixes for these three cases.
319                String groupSortKeyPortion;
320                if (groupSortKey == null) {
321                    groupSortKeyPortion = "nsk";
322                } else if (groupSortKey.equals("")) {
323                    groupSortKeyPortion = "esk";
324                } else {
325                    groupSortKeyPortion = "gsk=" + groupSortKey;
326                }
327
328                boolean isGroupSummary = record.getNotification().isGroupSummary();
329                record.setGlobalSortKey(
330                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
331                        record.isRecentlyIntrusive() ? '0' : '1',
332                        groupProxy.getAuthoritativeRank(),
333                        isGroupSummary ? '0' : '1',
334                        groupSortKeyPortion,
335                        record.getAuthoritativeRank()));
336            }
337            mProxyByGroupTmp.clear();
338        }
339
340        // Do a second ranking pass, using group proxies
341        Collections.sort(notificationList, mFinalComparator);
342    }
343
344    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
345        return Collections.binarySearch(notificationList, target, mFinalComparator);
346    }
347
348    private static int safeInt(XmlPullParser parser, String att, int defValue) {
349        final String val = parser.getAttributeValue(null, att);
350        return tryParseInt(val, defValue);
351    }
352
353    private static int tryParseInt(String value, int defValue) {
354        if (TextUtils.isEmpty(value)) return defValue;
355        try {
356            return Integer.valueOf(value);
357        } catch (NumberFormatException e) {
358            return defValue;
359        }
360    }
361
362    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
363        final String val = parser.getAttributeValue(null, att);
364        return tryParseBool(val, defValue);
365    }
366
367    private static boolean tryParseBool(String value, boolean defValue) {
368        if (TextUtils.isEmpty(value)) return defValue;
369        return Boolean.valueOf(value);
370    }
371
372    @Override
373    public List<Notification.Topic> getTopics(String packageName, int uid) {
374        final Record r = getOrCreateRecord(packageName, uid);
375        List<Notification.Topic> topics = new ArrayList<>();
376        for (Topic t :  r.topics.values()) {
377            topics.add(t.topic);
378        }
379        return topics;
380    }
381
382    @Override
383    public int getTopicPriority(String packageName, int uid, Notification.Topic topic) {
384        final Record r = getOrCreateRecord(packageName, uid);
385        return getOrCreateTopic(r, topic).priority;
386    }
387
388    @Override
389    public void setTopicPriority(String packageName, int uid, Notification.Topic topic,
390            int priority) {
391        final Record r = getOrCreateRecord(packageName, uid);
392        getOrCreateTopic(r, topic).priority = priority;
393        updateConfig();
394    }
395
396    @Override
397    public int getTopicVisibilityOverride(String packageName, int uid, Notification.Topic topic) {
398        final Record r = getOrCreateRecord(packageName, uid);
399        return getOrCreateTopic(r, topic).visibility;
400    }
401
402    @Override
403    public void setTopicVisibilityOverride(String pkgName, int uid, Notification.Topic topic,
404        int visibility) {
405        final Record r = getOrCreateRecord(pkgName, uid);
406        getOrCreateTopic(r, topic).visibility = visibility;
407        updateConfig();
408    }
409
410    @Override
411    public int getTopicImportance(String packageName, int uid, Notification.Topic topic) {
412        final Record r = getOrCreateRecord(packageName, uid);
413        return getOrCreateTopic(r, topic).importance;
414    }
415
416    @Override
417    public void setTopicImportance(String pkgName, int uid, Notification.Topic topic,
418            int importance) {
419        final Record r = getOrCreateRecord(pkgName, uid);
420        getOrCreateTopic(r, topic).importance = importance;
421        updateConfig();
422    }
423
424    /**
425     * Sets the default importance for all new topics that appear in the future, and resets
426     * the importance of all current topics.
427     */
428    @Override
429    public void setAppImportance(String pkgName, int uid, int importance) {
430        final Record r = getOrCreateRecord(pkgName, uid);
431        r.importance = importance;
432        for (Topic t :  r.topics.values()) {
433            t.importance = importance;
434        }
435        updateConfig();
436    }
437
438    @Override
439    public boolean doesAppUseTopics(String pkgName, int uid) {
440        final Record r = getOrCreateRecord(pkgName, uid);
441        int numTopics = r.topics.size();
442        if (numTopics == 0
443                || (numTopics == 1 && r.topics.containsKey(Notification.TOPIC_DEFAULT))) {
444            return false;
445        } else {
446            return true;
447        }
448    }
449
450    private Topic getOrCreateTopic(Record r, Notification.Topic topic) {
451        if (topic == null) {
452            topic = createDefaultTopic();
453        }
454        Topic t = r.topics.get(topic.getId());
455        if (t != null) {
456            return t;
457        } else {
458            t = new Topic(topic);
459            t.importance = r.importance;
460            r.topics.put(topic.getId(), t);
461            return t;
462        }
463    }
464
465    private Notification.Topic createDefaultTopic() {
466        return new Notification.Topic(Notification.TOPIC_DEFAULT,
467                mContext.getString(R.string.default_notification_topic_label));
468    }
469
470    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
471        if (filter == null) {
472            final int N = mSignalExtractors.length;
473            pw.print(prefix);
474            pw.print("mSignalExtractors.length = ");
475            pw.println(N);
476            for (int i = 0; i < N; i++) {
477                pw.print(prefix);
478                pw.print("  ");
479                pw.println(mSignalExtractors[i]);
480            }
481        }
482        if (filter == null) {
483            pw.print(prefix);
484            pw.println("per-package config:");
485        }
486        dumpRecords(pw, prefix, filter, mRecords);
487        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
488    }
489
490    private static void dumpRecords(PrintWriter pw, String prefix,
491            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
492        final int N = records.size();
493        for (int i = 0; i < N; i++) {
494            final Record r = records.valueAt(i);
495            if (filter == null || filter.matches(r.pkg)) {
496                pw.print(prefix);
497                pw.print("  ");
498                pw.print(r.pkg);
499                pw.print(" (");
500                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
501                pw.print(')');
502                pw.print(" importance=");
503                pw.print(Ranking.importanceToString(r.importance));
504                pw.println();
505                for (Topic t : r.topics.values()) {
506                    pw.print(prefix);
507                    pw.print("  ");
508                    pw.print("  ");
509                    pw.print(t.topic.getId());
510                    if (t.priority != DEFAULT_PRIORITY) {
511                        pw.print(" priority=");
512                        pw.print(Notification.priorityToString(t.priority));
513                    }
514                    if (t.visibility != DEFAULT_VISIBILITY) {
515                        pw.print(" visibility=");
516                        pw.print(Notification.visibilityToString(t.visibility));
517                    }
518                    if (t.importance != DEFAULT_IMPORTANCE) {
519                        pw.print(" importance=");
520                        pw.print(Ranking.importanceToString(t.importance));
521                    }
522                    pw.println();
523                }
524            }
525        }
526    }
527
528    public void onPackagesChanged(boolean queryReplace, String[] pkgList) {
529        if (queryReplace || pkgList == null || pkgList.length == 0
530                || mRestoredWithoutUids.isEmpty()) {
531            return; // nothing to do
532        }
533        final PackageManager pm = mContext.getPackageManager();
534        boolean updated = false;
535        for (String pkg : pkgList) {
536            final Record r = mRestoredWithoutUids.get(pkg);
537            if (r != null) {
538                try {
539                    //TODO: http://b/22388012
540                    r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
541                    mRestoredWithoutUids.remove(pkg);
542                    mRecords.put(recordKey(r.pkg, r.uid), r);
543                    updated = true;
544                } catch (NameNotFoundException e) {
545                    // noop
546                }
547            }
548        }
549        if (updated) {
550            updateConfig();
551        }
552    }
553
554    private static class Record {
555        static int UNKNOWN_UID = UserHandle.USER_NULL;
556
557        String pkg;
558        int uid = UNKNOWN_UID;
559        int importance = DEFAULT_IMPORTANCE;
560        Map<String, Topic> topics = new ArrayMap<>();
561   }
562
563    private static class Topic {
564        Notification.Topic topic;
565        int priority = DEFAULT_PRIORITY;
566        int visibility = DEFAULT_VISIBILITY;
567        int importance = DEFAULT_IMPORTANCE;
568
569        public Topic(Notification.Topic topic) {
570            this.topic = topic;
571        }
572    }
573}
574