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