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