RankingHelper.java revision 2b122f4c2e691f0319e4f9ea5873989792bb56a6
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.text.TextUtils;
24import android.util.ArrayMap;
25import android.util.Log;
26import android.util.Slog;
27import android.util.SparseIntArray;
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.Comparator;
37import java.util.concurrent.TimeUnit;
38
39public class RankingHelper implements RankingConfig {
40    private static final String TAG = "RankingHelper";
41    private static final boolean DEBUG = false;
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
53    private final NotificationSignalExtractor[] mSignalExtractors;
54    private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
55    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
56
57    // Package name to uid, to priority. Would be better as Table<String, Int, Int>
58    private final ArrayMap<String, SparseIntArray> mPackagePriorities;
59    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp;
60
61    private final Context mContext;
62    private final Handler mRankingHandler;
63
64    public RankingHelper(Context context, Handler rankingHandler, String[] extractorNames) {
65        mContext = context;
66        mRankingHandler = rankingHandler;
67        mPackagePriorities = new ArrayMap<String, SparseIntArray>();
68
69        final int N = extractorNames.length;
70        mSignalExtractors = new NotificationSignalExtractor[N];
71        for (int i = 0; i < N; i++) {
72            try {
73                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
74                NotificationSignalExtractor extractor =
75                        (NotificationSignalExtractor) extractorClass.newInstance();
76                extractor.initialize(mContext);
77                extractor.setConfig(this);
78                mSignalExtractors[i] = extractor;
79            } catch (ClassNotFoundException e) {
80                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
81            } catch (InstantiationException e) {
82                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
83            } catch (IllegalAccessException e) {
84                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
85            }
86        }
87        mProxyByGroupTmp = new ArrayMap<String, NotificationRecord>();
88    }
89
90    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
91        final int N = mSignalExtractors.length;
92        for (int i = 0; i < N; i++) {
93            final NotificationSignalExtractor extractor = mSignalExtractors[i];
94            if (extractorClass.equals(extractor.getClass())) {
95                return (T) extractor;
96            }
97        }
98        return null;
99    }
100
101    public void extractSignals(NotificationRecord r) {
102        final int N = mSignalExtractors.length;
103        for (int i = 0; i < N; i++) {
104            NotificationSignalExtractor extractor = mSignalExtractors[i];
105            try {
106                RankingReconsideration recon = extractor.process(r);
107                if (recon != null) {
108                    Message m = Message.obtain(mRankingHandler,
109                            NotificationManagerService.MESSAGE_RECONSIDER_RANKING, recon);
110                    long delay = recon.getDelay(TimeUnit.MILLISECONDS);
111                    mRankingHandler.sendMessageDelayed(m, delay);
112                }
113            } catch (Throwable t) {
114                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
115            }
116        }
117    }
118
119    public void readXml(XmlPullParser parser) throws XmlPullParserException, IOException {
120        int type = parser.getEventType();
121        if (type != XmlPullParser.START_TAG) return;
122        String tag = parser.getName();
123        if (!TAG_RANKING.equals(tag)) return;
124        mPackagePriorities.clear();
125        final int version = safeInt(parser, ATT_VERSION, XML_VERSION);
126        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
127            tag = parser.getName();
128            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
129                return;
130            }
131            if (type == XmlPullParser.START_TAG) {
132                if (TAG_PACKAGE.equals(tag)) {
133                    int uid = safeInt(parser, ATT_UID, UserHandle.USER_ALL);
134                    int priority = safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT);
135                    String name = parser.getAttributeValue(null, ATT_NAME);
136
137                    if (!TextUtils.isEmpty(name) && priority != Notification.PRIORITY_DEFAULT) {
138                        SparseIntArray priorityByUid = mPackagePriorities.get(name);
139                        if (priorityByUid == null) {
140                            priorityByUid = new SparseIntArray();
141                            mPackagePriorities.put(name, priorityByUid);
142                        }
143                        priorityByUid.put(uid, priority);
144                    }
145                }
146            }
147        }
148        throw new IllegalStateException("Failed to reach END_DOCUMENT");
149    }
150
151    public void writeXml(XmlSerializer out) throws IOException {
152        out.startTag(null, TAG_RANKING);
153        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
154
155        final int N = mPackagePriorities.size();
156        for (int i = 0; i < N; i ++) {
157            String name = mPackagePriorities.keyAt(i);
158            SparseIntArray priorityByUid = mPackagePriorities.get(name);
159            final int M = priorityByUid.size();
160            for (int j = 0; j < M; j++) {
161                int uid = priorityByUid.keyAt(j);
162                int priority = priorityByUid.get(uid);
163                out.startTag(null, TAG_PACKAGE);
164                out.attribute(null, ATT_NAME, name);
165                out.attribute(null, ATT_UID, Integer.toString(uid));
166                out.attribute(null, ATT_PRIORITY, Integer.toString(priority));
167                out.endTag(null, TAG_PACKAGE);
168            }
169        }
170        out.endTag(null, TAG_RANKING);
171    }
172
173    private void updateConfig() {
174        final int N = mSignalExtractors.length;
175        for (int i = 0; i < N; i++) {
176            mSignalExtractors[i].setConfig(this);
177        }
178        mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE);
179    }
180
181    public void sort(ArrayList<NotificationRecord> notificationList) {
182        final int N = notificationList.size();
183        // clear global sort keys
184        for (int i = N - 1; i >= 0; i--) {
185            notificationList.get(i).setGlobalSortKey(null);
186        }
187
188        try {
189            // rank each record individually
190            Collections.sort(notificationList, mPreliminaryComparator);
191        } catch (RuntimeException ex) {
192            // Don't crash the system server if something bad happened.
193            Log.e(TAG, "Extreme badness during notification sort", ex);
194            Log.e(TAG, "Current notification list: ");
195            for (int i = 0; i < N; i++) {
196                NotificationRecord nr = notificationList.get(i);
197                Log.e(TAG, String.format(
198                        "  [%d] %s (group %s, rank %d, sortkey %s)",
199                        i, nr, nr.getGroupKey(), nr.getAuthoritativeRank(),
200                        nr.getNotification().getSortKey()));
201            }
202            // STOPSHIP: remove once b/16626175 is found
203            throw ex;
204        }
205
206        synchronized (mProxyByGroupTmp) {
207            // record individual ranking result and nominate proxies for each group
208            for (int i = N - 1; i >= 0; i--) {
209                final NotificationRecord record = notificationList.get(i);
210                record.setAuthoritativeRank(i);
211                final String groupKey = record.getGroupKey();
212                boolean isGroupSummary = record.getNotification().isGroupSummary();
213                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
214                    mProxyByGroupTmp.put(groupKey, record);
215                }
216            }
217            // assign global sort key:
218            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
219            for (int i = 0; i < N; i++) {
220                final NotificationRecord record = notificationList.get(i);
221                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
222                String groupSortKey = record.getNotification().getSortKey();
223
224                // We need to make sure the developer provided group sort key (gsk) is handled
225                // correctly:
226                //   gsk="" < gsk=non-null-string < gsk=null
227                //
228                // We enforce this by using different prefixes for these three cases.
229                String groupSortKeyPortion;
230                if (groupSortKey == null) {
231                    groupSortKeyPortion = "nsk";
232                } else if (groupSortKey.equals("")) {
233                    groupSortKeyPortion = "esk";
234                } else {
235                    groupSortKeyPortion = "gsk=" + groupSortKey;
236                }
237
238                boolean isGroupSummary = record.getNotification().isGroupSummary();
239                record.setGlobalSortKey(
240                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
241                        record.isRecentlyIntrusive() ? '0' : '1',
242                        groupProxy.getAuthoritativeRank(),
243                        isGroupSummary ? '0' : '1',
244                        groupSortKeyPortion,
245                        record.getAuthoritativeRank()));
246            }
247            mProxyByGroupTmp.clear();
248        }
249
250        // Do a second ranking pass, using group proxies
251        Collections.sort(notificationList, mFinalComparator);
252    }
253
254    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
255        return Collections.binarySearch(notificationList, target, mFinalComparator);
256    }
257
258    private static int safeInt(XmlPullParser parser, String att, int defValue) {
259        final String val = parser.getAttributeValue(null, att);
260        return tryParseInt(val, defValue);
261    }
262
263    private static int tryParseInt(String value, int defValue) {
264        if (TextUtils.isEmpty(value)) return defValue;
265        try {
266            return Integer.valueOf(value);
267        } catch (NumberFormatException e) {
268            return defValue;
269        }
270    }
271
272    @Override
273    public int getPackagePriority(String packageName, int uid) {
274        int priority = Notification.PRIORITY_DEFAULT;
275        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
276        if (priorityByUid != null) {
277            priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT);
278        }
279        return priority;
280    }
281
282    @Override
283    public void setPackagePriority(String packageName, int uid, int priority) {
284        if (priority == getPackagePriority(packageName, uid)) {
285            return;
286        }
287        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
288        if (priorityByUid == null) {
289            priorityByUid = new SparseIntArray();
290            mPackagePriorities.put(packageName, priorityByUid);
291        }
292        priorityByUid.put(uid, priority);
293        updateConfig();
294    }
295
296    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
297        if (filter == null) {
298            final int N = mSignalExtractors.length;
299            pw.print(prefix);
300            pw.print("mSignalExtractors.length = ");
301            pw.println(N);
302            for (int i = 0; i < N; i++) {
303                pw.print(prefix);
304                pw.print("  ");
305                pw.println(mSignalExtractors[i]);
306            }
307        }
308        final int N = mPackagePriorities.size();
309        if (filter == null) {
310            pw.print(prefix);
311            pw.println("package priorities:");
312        }
313        for (int i = 0; i < N; i++) {
314            String name = mPackagePriorities.keyAt(i);
315            if (filter == null || filter.matches(name)) {
316                SparseIntArray priorityByUid = mPackagePriorities.get(name);
317                final int M = priorityByUid.size();
318                for (int j = 0; j < M; j++) {
319                    int uid = priorityByUid.keyAt(j);
320                    int priority = priorityByUid.get(uid);
321                    pw.print(prefix);
322                    pw.print("  ");
323                    pw.print(name);
324                    pw.print(" (");
325                    pw.print(uid);
326                    pw.print(") has priority: ");
327                    pw.println(priority);
328                }
329            }
330        }
331    }
332}
333