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