RankingHelper.java revision 92d456e9a62d7dadb3e21f481f7453bd82b1aa23
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                        r.priority = priority;
168                        r.visibility = vis;
169
170                        // Migrate package level settings to the default topic.
171                        // Might be overwritten by parseTopics.
172                        Topic defaultTopic = r.topics.get(Notification.TOPIC_DEFAULT);
173                        defaultTopic.priority = priority;
174                        defaultTopic.visibility = vis;
175
176                        parseTopics(r, parser);
177                    }
178                }
179            }
180        }
181        throw new IllegalStateException("Failed to reach END_DOCUMENT");
182    }
183
184    public void parseTopics(Record r, XmlPullParser parser)
185            throws XmlPullParserException, IOException {
186        final int innerDepth = parser.getDepth();
187        int type;
188        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
189                && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
190            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
191                continue;
192            }
193
194            String tagName = parser.getName();
195            if (TAG_TOPIC.equals(tagName)) {
196                int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
197                int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
198                int importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
199                String id = parser.getAttributeValue(null, ATT_TOPIC_ID);
200                CharSequence label = parser.getAttributeValue(null, ATT_TOPIC_LABEL);
201
202                if (!TextUtils.isEmpty(id)) {
203                    Topic topic = new Topic(new Notification.Topic(id, label));
204
205                    if (priority != DEFAULT_PRIORITY) {
206                        topic.priority = priority;
207                    }
208                    if (vis != DEFAULT_VISIBILITY) {
209                        topic.visibility = vis;
210                    }
211                    if (importance != DEFAULT_IMPORTANCE) {
212                        topic.importance = importance;
213                    }
214                    r.topics.put(id, topic);
215                }
216            }
217        }
218    }
219
220    private static String recordKey(String pkg, int uid) {
221        return pkg + "|" + uid;
222    }
223
224    private Record getOrCreateRecord(String pkg, int uid) {
225        final String key = recordKey(pkg, uid);
226        Record r = mRecords.get(key);
227        if (r == null) {
228            r = new Record();
229            r.pkg = pkg;
230            r.uid = uid;
231            r.topics.put(Notification.TOPIC_DEFAULT, new Topic(createDefaultTopic()));
232            mRecords.put(key, r);
233        }
234        return r;
235    }
236
237    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
238        out.startTag(null, TAG_RANKING);
239        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
240
241        final int N = mRecords.size();
242        for (int i = 0; i < N; i++) {
243            final Record r = mRecords.valueAt(i);
244            //TODO: http://b/22388012
245            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
246                continue;
247            }
248            out.startTag(null, TAG_PACKAGE);
249            out.attribute(null, ATT_NAME, r.pkg);
250            if (r.importance != DEFAULT_IMPORTANCE) {
251                out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
252            }
253            if (r.priority != DEFAULT_PRIORITY) {
254                out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
255            }
256            if (r.visibility != DEFAULT_VISIBILITY) {
257                out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
258            }
259
260            if (!forBackup) {
261                out.attribute(null, ATT_UID, Integer.toString(r.uid));
262            }
263
264            writeTopicsXml(out, r);
265            out.endTag(null, TAG_PACKAGE);
266        }
267        out.endTag(null, TAG_RANKING);
268    }
269
270    public void writeTopicsXml(XmlSerializer out, Record r) throws IOException {
271        for (Topic t : r.topics.values()) {
272            out.startTag(null, TAG_TOPIC);
273            out.attribute(null, ATT_TOPIC_ID, t.topic.getId());
274            out.attribute(null, ATT_TOPIC_LABEL, t.topic.getLabel().toString());
275            if (t.priority != DEFAULT_PRIORITY) {
276                out.attribute(null, ATT_PRIORITY, Integer.toString(t.priority));
277            }
278            if (t.visibility != DEFAULT_VISIBILITY) {
279                out.attribute(null, ATT_VISIBILITY, Integer.toString(t.visibility));
280            }
281            if (t.importance != DEFAULT_IMPORTANCE) {
282                out.attribute(null, ATT_IMPORTANCE, Integer.toString(t.importance));
283            }
284            out.endTag(null, TAG_TOPIC);
285        }
286    }
287
288    private void updateConfig() {
289        final int N = mSignalExtractors.length;
290        for (int i = 0; i < N; i++) {
291            mSignalExtractors[i].setConfig(this);
292        }
293        mRankingHandler.requestSort();
294    }
295
296    public void sort(ArrayList<NotificationRecord> notificationList) {
297        final int N = notificationList.size();
298        // clear global sort keys
299        for (int i = N - 1; i >= 0; i--) {
300            notificationList.get(i).setGlobalSortKey(null);
301        }
302
303        // rank each record individually
304        Collections.sort(notificationList, mPreliminaryComparator);
305
306        synchronized (mProxyByGroupTmp) {
307            // record individual ranking result and nominate proxies for each group
308            for (int i = N - 1; i >= 0; i--) {
309                final NotificationRecord record = notificationList.get(i);
310                record.setAuthoritativeRank(i);
311                final String groupKey = record.getGroupKey();
312                boolean isGroupSummary = record.getNotification().isGroupSummary();
313                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
314                    mProxyByGroupTmp.put(groupKey, record);
315                }
316            }
317            // assign global sort key:
318            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
319            for (int i = 0; i < N; i++) {
320                final NotificationRecord record = notificationList.get(i);
321                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
322                String groupSortKey = record.getNotification().getSortKey();
323
324                // We need to make sure the developer provided group sort key (gsk) is handled
325                // correctly:
326                //   gsk="" < gsk=non-null-string < gsk=null
327                //
328                // We enforce this by using different prefixes for these three cases.
329                String groupSortKeyPortion;
330                if (groupSortKey == null) {
331                    groupSortKeyPortion = "nsk";
332                } else if (groupSortKey.equals("")) {
333                    groupSortKeyPortion = "esk";
334                } else {
335                    groupSortKeyPortion = "gsk=" + groupSortKey;
336                }
337
338                boolean isGroupSummary = record.getNotification().isGroupSummary();
339                record.setGlobalSortKey(
340                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
341                        record.isRecentlyIntrusive() ? '0' : '1',
342                        groupProxy.getAuthoritativeRank(),
343                        isGroupSummary ? '0' : '1',
344                        groupSortKeyPortion,
345                        record.getAuthoritativeRank()));
346            }
347            mProxyByGroupTmp.clear();
348        }
349
350        // Do a second ranking pass, using group proxies
351        Collections.sort(notificationList, mFinalComparator);
352    }
353
354    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
355        return Collections.binarySearch(notificationList, target, mFinalComparator);
356    }
357
358    private static int safeInt(XmlPullParser parser, String att, int defValue) {
359        final String val = parser.getAttributeValue(null, att);
360        return tryParseInt(val, defValue);
361    }
362
363    private static int tryParseInt(String value, int defValue) {
364        if (TextUtils.isEmpty(value)) return defValue;
365        try {
366            return Integer.valueOf(value);
367        } catch (NumberFormatException e) {
368            return defValue;
369        }
370    }
371
372    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
373        final String val = parser.getAttributeValue(null, att);
374        return tryParseBool(val, defValue);
375    }
376
377    private static boolean tryParseBool(String value, boolean defValue) {
378        if (TextUtils.isEmpty(value)) return defValue;
379        return Boolean.valueOf(value);
380    }
381
382    @Override
383    public List<Notification.Topic> getTopics(String packageName, int uid) {
384        final Record r = getOrCreateRecord(packageName, uid);
385        List<Notification.Topic> topics = new ArrayList<>();
386        for (Topic t : r.topics.values()) {
387            topics.add(t.topic);
388        }
389        return topics;
390    }
391
392    /**
393     * Gets priority. If a topic is given, returns the priority of that topic. Otherwise, the
394     * priority of the app.
395     */
396    @Override
397    public int getPriority(String packageName, int uid, Notification.Topic topic) {
398        final Record r = getOrCreateRecord(packageName, uid);
399        if (topic == null) {
400            return r.priority;
401        }
402        return getOrCreateTopic(r, topic).priority;
403    }
404
405    /**
406     * Sets priority. If a topic is given, sets the priority of that topic. If not,
407     * sets the default priority for all new topics that appear in the future, and resets
408     * the priority of all current topics.
409     */
410    @Override
411    public void setPriority(String packageName, int uid, Notification.Topic topic,
412            int priority) {
413        final Record r = getOrCreateRecord(packageName, uid);
414        if (topic == null) {
415            r.priority = priority;
416            for (Topic t : r.topics.values()) {
417                t.priority = priority;
418            }
419        } else {
420            getOrCreateTopic(r, topic).priority = priority;
421        }
422        updateConfig();
423    }
424
425    /**
426     * Gets visual override. If a topic is given, returns the override of that topic. Otherwise, the
427     * override of the app.
428     */
429    @Override
430    public int getVisibilityOverride(String packageName, int uid, Notification.Topic topic) {
431        final Record r = getOrCreateRecord(packageName, uid);
432        if (topic == null) {
433            return r.visibility;
434        }
435        return getOrCreateTopic(r, topic).visibility;
436    }
437
438    /**
439     * Sets visibility override. If a topic is given, sets the override of that topic. If not,
440     * sets the default override for all new topics that appear in the future, and resets
441     * the override of all current topics.
442     */
443    @Override
444    public void setVisibilityOverride(String pkgName, int uid, Notification.Topic topic,
445        int visibility) {
446        final Record r = getOrCreateRecord(pkgName, uid);
447        if (topic == null) {
448            r.visibility = visibility;
449            for (Topic t : r.topics.values()) {
450                t.visibility = visibility;
451            }
452        } else {
453            getOrCreateTopic(r, topic).visibility = visibility;
454        }
455        updateConfig();
456    }
457
458    /**
459     * Gets importance. If a topic is given, returns the importance of that topic. Otherwise, the
460     * importance of the app.
461     */
462    @Override
463    public int getImportance(String packageName, int uid, Notification.Topic topic) {
464        final Record r = getOrCreateRecord(packageName, uid);
465        if (topic == null) {
466            return r.importance;
467        }
468        return getOrCreateTopic(r, topic).importance;
469    }
470
471    /**
472     * Sets importance. If a topic is given, sets the importance of that topic. If not, sets the
473     * default importance for all new topics that appear in the future, and resets
474     * the importance of all current topics (unless the app is being blocked).
475     */
476    @Override
477    public void setImportance(String pkgName, int uid, Notification.Topic topic,
478            int importance) {
479        final Record r = getOrCreateRecord(pkgName, uid);
480        if (topic == null) {
481            r.importance = importance;
482            if (Ranking.IMPORTANCE_NONE != importance) {
483                for (Topic t : r.topics.values()) {
484                    t.importance = importance;
485                }
486            }
487        } else {
488            getOrCreateTopic(r, topic).importance = importance;
489        }
490        updateConfig();
491    }
492
493    @Override
494    public boolean doesAppUseTopics(String pkgName, int uid) {
495        final Record r = getOrCreateRecord(pkgName, uid);
496        int numTopics = r.topics.size();
497        if (numTopics == 0
498                || (numTopics == 1 && r.topics.containsKey(Notification.TOPIC_DEFAULT))) {
499            return false;
500        } else {
501            return true;
502        }
503    }
504
505    private Topic getOrCreateTopic(Record r, Notification.Topic topic) {
506        if (topic == null) {
507            topic = createDefaultTopic();
508        }
509        Topic t = r.topics.get(topic.getId());
510        if (t != null) {
511            return t;
512        } else {
513            t = new Topic(topic);
514            t.importance = r.importance;
515            t.priority = r.priority;
516            t.visibility = r.visibility;
517            r.topics.put(topic.getId(), t);
518            return t;
519        }
520    }
521
522    private Notification.Topic createDefaultTopic() {
523        return new Notification.Topic(Notification.TOPIC_DEFAULT,
524                mContext.getString(R.string.default_notification_topic_label));
525    }
526
527    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
528        if (filter == null) {
529            final int N = mSignalExtractors.length;
530            pw.print(prefix);
531            pw.print("mSignalExtractors.length = ");
532            pw.println(N);
533            for (int i = 0; i < N; i++) {
534                pw.print(prefix);
535                pw.print("  ");
536                pw.println(mSignalExtractors[i]);
537            }
538        }
539        if (filter == null) {
540            pw.print(prefix);
541            pw.println("per-package config:");
542        }
543        pw.println("Records:");
544        dumpRecords(pw, prefix, filter, mRecords);
545        pw.println("Restored without uid:");
546        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
547    }
548
549    private static void dumpRecords(PrintWriter pw, String prefix,
550            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
551        final int N = records.size();
552        for (int i = 0; i < N; i++) {
553            final Record r = records.valueAt(i);
554            if (filter == null || filter.matches(r.pkg)) {
555                pw.print(prefix);
556                pw.print("  ");
557                pw.print(r.pkg);
558                pw.print(" (");
559                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
560                pw.print(')');
561                if (r.importance != DEFAULT_IMPORTANCE) {
562                    pw.print(" importance=");
563                    pw.print(Ranking.importanceToString(r.importance));
564                }
565                if (r.priority != DEFAULT_PRIORITY) {
566                    pw.print(" priority=");
567                    pw.print(Ranking.importanceToString(r.priority));
568                }
569                if (r.visibility != DEFAULT_VISIBILITY) {
570                    pw.print(" visibility=");
571                    pw.print(Ranking.importanceToString(r.visibility));
572                }
573                pw.println();
574                for (Topic t : r.topics.values()) {
575                    pw.print(prefix);
576                    pw.print("  ");
577                    pw.print("  ");
578                    pw.print(t.topic.getId());
579                    if (t.priority != DEFAULT_PRIORITY) {
580                        pw.print(" priority=");
581                        pw.print(Notification.priorityToString(t.priority));
582                    }
583                    if (t.visibility != DEFAULT_VISIBILITY) {
584                        pw.print(" visibility=");
585                        pw.print(Notification.visibilityToString(t.visibility));
586                    }
587                    if (t.importance != DEFAULT_IMPORTANCE) {
588                        pw.print(" importance=");
589                        pw.print(Ranking.importanceToString(t.importance));
590                    }
591                    pw.println();
592                }
593            }
594        }
595    }
596
597    public void onPackagesChanged(boolean queryReplace, String[] pkgList) {
598        if (queryReplace || pkgList == null || pkgList.length == 0
599                || mRestoredWithoutUids.isEmpty()) {
600            return; // nothing to do
601        }
602        final PackageManager pm = mContext.getPackageManager();
603        boolean updated = false;
604        for (String pkg : pkgList) {
605            final Record r = mRestoredWithoutUids.get(pkg);
606            if (r != null) {
607                try {
608                    //TODO: http://b/22388012
609                    r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
610                    mRestoredWithoutUids.remove(pkg);
611                    mRecords.put(recordKey(r.pkg, r.uid), r);
612                    updated = true;
613                } catch (NameNotFoundException e) {
614                    // noop
615                }
616            }
617        }
618        if (updated) {
619            updateConfig();
620        }
621    }
622
623    private static class Record {
624        static int UNKNOWN_UID = UserHandle.USER_NULL;
625
626        String pkg;
627        int uid = UNKNOWN_UID;
628        int importance = DEFAULT_IMPORTANCE;
629        int priority = DEFAULT_PRIORITY;
630        int visibility = DEFAULT_VISIBILITY;
631        Map<String, Topic> topics = new ArrayMap<>();
632   }
633
634    private static class Topic {
635        Notification.Topic topic;
636        int priority = DEFAULT_PRIORITY;
637        int visibility = DEFAULT_VISIBILITY;
638        int importance = DEFAULT_IMPORTANCE;
639
640        public Topic(Notification.Topic topic) {
641            this.topic = topic;
642        }
643    }
644}
645