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