RankingHelper.java revision bc253a28582c4cdd56cc8a0d49cd69b4624cfb4b
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 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.concurrent.TimeUnit;
39
40public class RankingHelper implements RankingConfig {
41    private static final String TAG = "RankingHelper";
42
43    private static final int XML_VERSION = 1;
44
45    private static final String TAG_RANKING = "ranking";
46    private static final String TAG_PACKAGE = "package";
47    private static final String ATT_VERSION = "version";
48
49    private static final String ATT_NAME = "name";
50    private static final String ATT_UID = "uid";
51    private static final String ATT_PRIORITY = "priority";
52    private static final String ATT_VISIBILITY = "visibility";
53
54    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
55    private static final int DEFAULT_VISIBILITY =
56            NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
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 Handler mRankingHandler;
68
69    public RankingHelper(Context context, Handler rankingHandler, NotificationUsageStats usageStats,
70            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                    Message m = Message.obtain(mRankingHandler,
114                            NotificationManagerService.MESSAGE_RECONSIDER_RANKING, recon);
115                    long delay = recon.getDelay(TimeUnit.MILLISECONDS);
116                    mRankingHandler.sendMessageDelayed(m, delay);
117                }
118            } catch (Throwable t) {
119                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
120            }
121        }
122    }
123
124    public void readXml(XmlPullParser parser, boolean forRestore)
125            throws XmlPullParserException, IOException {
126        final PackageManager pm = mContext.getPackageManager();
127        int type = parser.getEventType();
128        if (type != XmlPullParser.START_TAG) return;
129        String tag = parser.getName();
130        if (!TAG_RANKING.equals(tag)) return;
131        mRecords.clear();
132        mRestoredWithoutUids.clear();
133        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
134            tag = parser.getName();
135            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
136                return;
137            }
138            if (type == XmlPullParser.START_TAG) {
139                if (TAG_PACKAGE.equals(tag)) {
140                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
141                    int priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
142                    int vis = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
143                    String name = parser.getAttributeValue(null, ATT_NAME);
144
145                    if (!TextUtils.isEmpty(name)) {
146                        if (forRestore) {
147                            try {
148                                //TODO: http://b/22388012
149                                uid = pm.getPackageUid(name, UserHandle.USER_SYSTEM);
150                            } catch (NameNotFoundException e) {
151                                // noop
152                            }
153                        }
154                        Record r = null;
155                        if (uid == Record.UNKNOWN_UID) {
156                            r = mRestoredWithoutUids.get(name);
157                            if (r == null) {
158                                r = new Record();
159                                mRestoredWithoutUids.put(name, r);
160                            }
161                        } else {
162                            r = getOrCreateRecord(name, uid);
163                        }
164                        if (priority != DEFAULT_PRIORITY) {
165                            r.priority = priority;
166                        }
167                        if (vis != DEFAULT_VISIBILITY) {
168                            r.visibility = vis;
169                        }
170                    }
171                }
172            }
173        }
174        throw new IllegalStateException("Failed to reach END_DOCUMENT");
175    }
176
177    private static String recordKey(String pkg, int uid) {
178        return pkg + "|" + uid;
179    }
180
181    private Record getOrCreateRecord(String pkg, int uid) {
182        final String key = recordKey(pkg, uid);
183        Record r = mRecords.get(key);
184        if (r == null) {
185            r = new Record();
186            r.pkg = pkg;
187            r.uid = uid;
188            mRecords.put(key, r);
189        }
190        return r;
191    }
192
193    private void removeDefaultRecords() {
194        final int N = mRecords.size();
195        for (int i = N - 1; i >= 0; i--) {
196            final Record r = mRecords.valueAt(i);
197            if (r.priority == DEFAULT_PRIORITY && r.visibility == DEFAULT_VISIBILITY) {
198                mRecords.remove(i);
199            }
200        }
201    }
202
203    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
204        out.startTag(null, TAG_RANKING);
205        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
206
207        final int N = mRecords.size();
208        for (int i = 0; i < N; i++) {
209            final Record r = mRecords.valueAt(i);
210            //TODO: http://b/22388012
211            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
212                continue;
213            }
214            out.startTag(null, TAG_PACKAGE);
215            out.attribute(null, ATT_NAME, r.pkg);
216            if (r.priority != DEFAULT_PRIORITY) {
217                out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
218            }
219            if (r.visibility != DEFAULT_VISIBILITY) {
220                out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
221            }
222            if (!forBackup) {
223                out.attribute(null, ATT_UID, Integer.toString(r.uid));
224            }
225            out.endTag(null, TAG_PACKAGE);
226        }
227        out.endTag(null, TAG_RANKING);
228    }
229
230    private void updateConfig() {
231        final int N = mSignalExtractors.length;
232        for (int i = 0; i < N; i++) {
233            mSignalExtractors[i].setConfig(this);
234        }
235        mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE);
236    }
237
238    public void sort(ArrayList<NotificationRecord> notificationList) {
239        final int N = notificationList.size();
240        // clear global sort keys
241        for (int i = N - 1; i >= 0; i--) {
242            notificationList.get(i).setGlobalSortKey(null);
243        }
244
245        // rank each record individually
246        Collections.sort(notificationList, mPreliminaryComparator);
247
248        synchronized (mProxyByGroupTmp) {
249            // record individual ranking result and nominate proxies for each group
250            for (int i = N - 1; i >= 0; i--) {
251                final NotificationRecord record = notificationList.get(i);
252                record.setAuthoritativeRank(i);
253                final String groupKey = record.getGroupKey();
254                boolean isGroupSummary = record.getNotification().isGroupSummary();
255                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
256                    mProxyByGroupTmp.put(groupKey, record);
257                }
258            }
259            // assign global sort key:
260            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
261            for (int i = 0; i < N; i++) {
262                final NotificationRecord record = notificationList.get(i);
263                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
264                String groupSortKey = record.getNotification().getSortKey();
265
266                // We need to make sure the developer provided group sort key (gsk) is handled
267                // correctly:
268                //   gsk="" < gsk=non-null-string < gsk=null
269                //
270                // We enforce this by using different prefixes for these three cases.
271                String groupSortKeyPortion;
272                if (groupSortKey == null) {
273                    groupSortKeyPortion = "nsk";
274                } else if (groupSortKey.equals("")) {
275                    groupSortKeyPortion = "esk";
276                } else {
277                    groupSortKeyPortion = "gsk=" + groupSortKey;
278                }
279
280                boolean isGroupSummary = record.getNotification().isGroupSummary();
281                record.setGlobalSortKey(
282                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
283                        record.isRecentlyIntrusive() ? '0' : '1',
284                        groupProxy.getAuthoritativeRank(),
285                        isGroupSummary ? '0' : '1',
286                        groupSortKeyPortion,
287                        record.getAuthoritativeRank()));
288            }
289            mProxyByGroupTmp.clear();
290        }
291
292        // Do a second ranking pass, using group proxies
293        Collections.sort(notificationList, mFinalComparator);
294    }
295
296    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
297        return Collections.binarySearch(notificationList, target, mFinalComparator);
298    }
299
300    private static int safeInt(XmlPullParser parser, String att, int defValue) {
301        final String val = parser.getAttributeValue(null, att);
302        return tryParseInt(val, defValue);
303    }
304
305    private static int tryParseInt(String value, int defValue) {
306        if (TextUtils.isEmpty(value)) return defValue;
307        try {
308            return Integer.valueOf(value);
309        } catch (NumberFormatException e) {
310            return defValue;
311        }
312    }
313
314    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
315        final String val = parser.getAttributeValue(null, att);
316        return tryParseBool(val, defValue);
317    }
318
319    private static boolean tryParseBool(String value, boolean defValue) {
320        if (TextUtils.isEmpty(value)) return defValue;
321        return Boolean.valueOf(value);
322    }
323
324    @Override
325    public int getPackagePriority(String packageName, int uid) {
326        final Record r = mRecords.get(recordKey(packageName, uid));
327        return r != null ? r.priority : DEFAULT_PRIORITY;
328    }
329
330    @Override
331    public void setPackagePriority(String packageName, int uid, int priority) {
332        if (priority == getPackagePriority(packageName, uid)) {
333            return;
334        }
335        getOrCreateRecord(packageName, uid).priority = priority;
336        removeDefaultRecords();
337        updateConfig();
338    }
339
340    @Override
341    public int getPackageVisibilityOverride(String packageName, int uid) {
342        final Record r = mRecords.get(recordKey(packageName, uid));
343        return r != null ? r.visibility : DEFAULT_VISIBILITY;
344    }
345
346    @Override
347    public void setPackageVisibilityOverride(String packageName, int uid, int visibility) {
348        if (visibility == getPackageVisibilityOverride(packageName, uid)) {
349            return;
350        }
351        getOrCreateRecord(packageName, uid).visibility = visibility;
352        removeDefaultRecords();
353        updateConfig();
354    }
355
356    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
357        if (filter == null) {
358            final int N = mSignalExtractors.length;
359            pw.print(prefix);
360            pw.print("mSignalExtractors.length = ");
361            pw.println(N);
362            for (int i = 0; i < N; i++) {
363                pw.print(prefix);
364                pw.print("  ");
365                pw.println(mSignalExtractors[i]);
366            }
367        }
368        if (filter == null) {
369            pw.print(prefix);
370            pw.println("per-package config:");
371        }
372        dumpRecords(pw, prefix, filter, mRecords);
373        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
374    }
375
376    private static void dumpRecords(PrintWriter pw, String prefix,
377            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
378        final int N = records.size();
379        for (int i = 0; i < N; i++) {
380            final Record r = records.valueAt(i);
381            if (filter == null || filter.matches(r.pkg)) {
382                pw.print(prefix);
383                pw.print("  ");
384                pw.print(r.pkg);
385                pw.print(" (");
386                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
387                pw.print(')');
388                if (r.priority != DEFAULT_PRIORITY) {
389                    pw.print(" priority=");
390                    pw.print(Notification.priorityToString(r.priority));
391                }
392                if (r.visibility != DEFAULT_VISIBILITY) {
393                    pw.print(" visibility=");
394                    pw.print(Notification.visibilityToString(r.visibility));
395                }
396                pw.println();
397            }
398        }
399    }
400
401    public void onPackagesChanged(boolean queryReplace, String[] pkgList) {
402        if (queryReplace || pkgList == null || pkgList.length == 0
403                || mRestoredWithoutUids.isEmpty()) {
404            return; // nothing to do
405        }
406        final PackageManager pm = mContext.getPackageManager();
407        boolean updated = false;
408        for (String pkg : pkgList) {
409            final Record r = mRestoredWithoutUids.get(pkg);
410            if (r != null) {
411                try {
412                    //TODO: http://b/22388012
413                    r.uid = pm.getPackageUid(r.pkg, UserHandle.USER_SYSTEM);
414                    mRestoredWithoutUids.remove(pkg);
415                    mRecords.put(recordKey(r.pkg, r.uid), r);
416                    updated = true;
417                } catch (NameNotFoundException e) {
418                    // noop
419                }
420            }
421        }
422        if (updated) {
423            updateConfig();
424        }
425    }
426
427    private static class Record {
428        static int UNKNOWN_UID = UserHandle.USER_NULL;
429
430        String pkg;
431        int uid = UNKNOWN_UID;
432        int priority = DEFAULT_PRIORITY;
433        int visibility = DEFAULT_VISIBILITY;
434    }
435
436}
437