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