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