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