RankingHelper.java revision dd3e86bcb0b6a5dc356d903df88c3d5a15510f7c
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    @Override
393    public boolean hasBannedTopics(String packageName, int uid) {
394        final Record r = getOrCreateRecord(packageName, uid);
395        for (Topic t : r.topics.values()) {
396            if (t.importance == Ranking.IMPORTANCE_NONE) {
397                return true;
398            }
399        }
400        return false;
401    }
402
403    /**
404     * Gets priority. If a topic is given, returns the priority of that topic. Otherwise, the
405     * priority of the app.
406     */
407    @Override
408    public int getPriority(String packageName, int uid, Notification.Topic topic) {
409        final Record r = getOrCreateRecord(packageName, uid);
410        if (topic == null) {
411            return r.priority;
412        }
413        return getOrCreateTopic(r, topic).priority;
414    }
415
416    /**
417     * Sets priority. If a topic is given, sets the priority of that topic. If not,
418     * sets the default priority for all new topics that appear in the future, and resets
419     * the priority of all current topics.
420     */
421    @Override
422    public void setPriority(String packageName, int uid, Notification.Topic topic,
423            int priority) {
424        final Record r = getOrCreateRecord(packageName, uid);
425        if (topic == null) {
426            r.priority = priority;
427            for (Topic t : r.topics.values()) {
428                t.priority = priority;
429            }
430        } else {
431            getOrCreateTopic(r, topic).priority = priority;
432        }
433        updateConfig();
434    }
435
436    /**
437     * Gets visual override. If a topic is given, returns the override of that topic. Otherwise, the
438     * override of the app.
439     */
440    @Override
441    public int getVisibilityOverride(String packageName, int uid, Notification.Topic topic) {
442        final Record r = getOrCreateRecord(packageName, uid);
443        if (topic == null) {
444            return r.visibility;
445        }
446        return getOrCreateTopic(r, topic).visibility;
447    }
448
449    /**
450     * Sets visibility override. If a topic is given, sets the override of that topic. If not,
451     * sets the default override for all new topics that appear in the future, and resets
452     * the override of all current topics.
453     */
454    @Override
455    public void setVisibilityOverride(String pkgName, int uid, Notification.Topic topic,
456        int visibility) {
457        final Record r = getOrCreateRecord(pkgName, uid);
458        if (topic == null) {
459            r.visibility = visibility;
460            for (Topic t : r.topics.values()) {
461                t.visibility = visibility;
462            }
463        } else {
464            getOrCreateTopic(r, topic).visibility = visibility;
465        }
466        updateConfig();
467    }
468
469    /**
470     * Gets importance. If a topic is given, returns the importance of that topic. Otherwise, the
471     * importance of the app.
472     */
473    @Override
474    public int getImportance(String packageName, int uid, Notification.Topic topic) {
475        final Record r = getOrCreateRecord(packageName, uid);
476        if (topic == null) {
477            return r.importance;
478        }
479        return getOrCreateTopic(r, topic).importance;
480    }
481
482    /**
483     * Sets importance. If a topic is given, sets the importance of that topic. If not, sets the
484     * default importance for all new topics that appear in the future, and resets
485     * the importance of all current topics (unless the app is being blocked).
486     */
487    @Override
488    public void setImportance(String pkgName, int uid, Notification.Topic topic,
489            int importance) {
490        final Record r = getOrCreateRecord(pkgName, uid);
491        if (topic == null) {
492            r.importance = importance;
493            if (Ranking.IMPORTANCE_NONE != importance) {
494                for (Topic t : r.topics.values()) {
495                    t.importance = importance;
496                }
497            }
498        } else {
499            getOrCreateTopic(r, topic).importance = importance;
500        }
501        updateConfig();
502    }
503
504    @Override
505    public boolean doesAppUseTopics(String pkgName, int uid) {
506        final Record r = getOrCreateRecord(pkgName, uid);
507        int numTopics = r.topics.size();
508        if (numTopics == 0
509                || (numTopics == 1 && r.topics.containsKey(Notification.TOPIC_DEFAULT))) {
510            return false;
511        } else {
512            return true;
513        }
514    }
515
516    private Topic getOrCreateTopic(Record r, Notification.Topic topic) {
517        if (topic == null) {
518            topic = createDefaultTopic();
519        }
520        Topic t = r.topics.get(topic.getId());
521        if (t != null) {
522            return t;
523        } else {
524            t = new Topic(topic);
525            t.importance = r.importance;
526            t.priority = r.priority;
527            t.visibility = r.visibility;
528            r.topics.put(topic.getId(), t);
529            return t;
530        }
531    }
532
533    private Notification.Topic createDefaultTopic() {
534        return new Notification.Topic(Notification.TOPIC_DEFAULT,
535                mContext.getString(R.string.default_notification_topic_label));
536    }
537
538    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
539        if (filter == null) {
540            final int N = mSignalExtractors.length;
541            pw.print(prefix);
542            pw.print("mSignalExtractors.length = ");
543            pw.println(N);
544            for (int i = 0; i < N; i++) {
545                pw.print(prefix);
546                pw.print("  ");
547                pw.println(mSignalExtractors[i]);
548            }
549        }
550        if (filter == null) {
551            pw.print(prefix);
552            pw.println("per-package config:");
553        }
554        pw.println("Records:");
555        dumpRecords(pw, prefix, filter, mRecords);
556        pw.println("Restored without uid:");
557        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
558    }
559
560    private static void dumpRecords(PrintWriter pw, String prefix,
561            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
562        final int N = records.size();
563        for (int i = 0; i < N; i++) {
564            final Record r = records.valueAt(i);
565            if (filter == null || filter.matches(r.pkg)) {
566                pw.print(prefix);
567                pw.print("  ");
568                pw.print(r.pkg);
569                pw.print(" (");
570                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
571                pw.print(')');
572                if (r.importance != DEFAULT_IMPORTANCE) {
573                    pw.print(" importance=");
574                    pw.print(Ranking.importanceToString(r.importance));
575                }
576                if (r.priority != DEFAULT_PRIORITY) {
577                    pw.print(" priority=");
578                    pw.print(Ranking.importanceToString(r.priority));
579                }
580                if (r.visibility != DEFAULT_VISIBILITY) {
581                    pw.print(" visibility=");
582                    pw.print(Ranking.importanceToString(r.visibility));
583                }
584                pw.println();
585                for (Topic t : r.topics.values()) {
586                    pw.print(prefix);
587                    pw.print("  ");
588                    pw.print("  ");
589                    pw.print(t.topic.getId());
590                    if (t.priority != DEFAULT_PRIORITY) {
591                        pw.print(" priority=");
592                        pw.print(Notification.priorityToString(t.priority));
593                    }
594                    if (t.visibility != DEFAULT_VISIBILITY) {
595                        pw.print(" visibility=");
596                        pw.print(Notification.visibilityToString(t.visibility));
597                    }
598                    if (t.importance != DEFAULT_IMPORTANCE) {
599                        pw.print(" importance=");
600                        pw.print(Ranking.importanceToString(t.importance));
601                    }
602                    pw.println();
603                }
604            }
605        }
606    }
607
608    public void onPackagesChanged(boolean queryReplace, String[] pkgList) {
609        if (queryReplace || pkgList == null || pkgList.length == 0
610                || mRestoredWithoutUids.isEmpty()) {
611            return; // nothing to do
612        }
613        final PackageManager pm = mContext.getPackageManager();
614        boolean updated = false;
615        for (String pkg : pkgList) {
616            final Record r = mRestoredWithoutUids.get(pkg);
617            if (r != null) {
618                try {
619                    //TODO: http://b/22388012
620                    r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
621                    mRestoredWithoutUids.remove(pkg);
622                    mRecords.put(recordKey(r.pkg, r.uid), r);
623                    updated = true;
624                } catch (NameNotFoundException e) {
625                    // noop
626                }
627            }
628        }
629        if (updated) {
630            updateConfig();
631        }
632    }
633
634    private static class Record {
635        static int UNKNOWN_UID = UserHandle.USER_NULL;
636
637        String pkg;
638        int uid = UNKNOWN_UID;
639        int importance = DEFAULT_IMPORTANCE;
640        int priority = DEFAULT_PRIORITY;
641        int visibility = DEFAULT_VISIBILITY;
642        Map<String, Topic> topics = new ArrayMap<>();
643   }
644
645    private static class Topic {
646        Notification.Topic topic;
647        int priority = DEFAULT_PRIORITY;
648        int visibility = DEFAULT_VISIBILITY;
649        int importance = DEFAULT_IMPORTANCE;
650
651        public Topic(Notification.Topic topic) {
652            this.topic = topic;
653        }
654    }
655}
656