RankingHelper.java revision 3ad4e3a45bbe44129b14c4d391431e44f1e04f0c
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.ArraySet;
27import android.util.Log;
28import android.util.Slog;
29import android.util.SparseIntArray;
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.Set;
39import java.util.concurrent.TimeUnit;
40
41public class RankingHelper implements RankingConfig {
42    private static final String TAG = "RankingHelper";
43    private static final boolean DEBUG = false;
44
45    private static final int XML_VERSION = 1;
46
47    private static final String TAG_RANKING = "ranking";
48    private static final String TAG_PACKAGE = "package";
49    private static final String ATT_VERSION = "version";
50
51    private static final String ATT_NAME = "name";
52    private static final String ATT_UID = "uid";
53    private static final String ATT_PRIORITY = "priority";
54    private static final String ATT_VISIBILITY = "visibility";
55
56    private final NotificationSignalExtractor[] mSignalExtractors;
57    private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
58    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
59
60    // Package name to uid, to priority. Would be better as Table<String, Int, Int>
61    private final ArrayMap<String, SparseIntArray> mPackagePriorities;
62    private final ArrayMap<String, SparseIntArray> mPackageVisibilities;
63    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp;
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        mPackagePriorities = new ArrayMap<String, SparseIntArray>();
72        mPackageVisibilities = new ArrayMap<String, SparseIntArray>();
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);
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        mProxyByGroupTmp = new ArrayMap<String, NotificationRecord>();
93    }
94
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) throws XmlPullParserException, IOException {
125        int type = parser.getEventType();
126        if (type != XmlPullParser.START_TAG) return;
127        String tag = parser.getName();
128        if (!TAG_RANKING.equals(tag)) return;
129        mPackagePriorities.clear();
130        final int version = safeInt(parser, ATT_VERSION, XML_VERSION);
131        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
132            tag = parser.getName();
133            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
134                return;
135            }
136            if (type == XmlPullParser.START_TAG) {
137                if (TAG_PACKAGE.equals(tag)) {
138                    int uid = safeInt(parser, ATT_UID, UserHandle.USER_ALL);
139                    int priority = safeInt(parser, ATT_PRIORITY, Notification.PRIORITY_DEFAULT);
140                    int vis = safeInt(parser, ATT_VISIBILITY,
141                            NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
142                    String name = parser.getAttributeValue(null, ATT_NAME);
143
144                    if (!TextUtils.isEmpty(name)) {
145                        if (priority != Notification.PRIORITY_DEFAULT) {
146                            SparseIntArray priorityByUid = mPackagePriorities.get(name);
147                            if (priorityByUid == null) {
148                                priorityByUid = new SparseIntArray();
149                                mPackagePriorities.put(name, priorityByUid);
150                            }
151                            priorityByUid.put(uid, priority);
152                        }
153                        if (vis != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) {
154                            SparseIntArray visibilityByUid = mPackageVisibilities.get(name);
155                            if (visibilityByUid == null) {
156                                visibilityByUid = new SparseIntArray();
157                                mPackageVisibilities.put(name, visibilityByUid);
158                            }
159                            visibilityByUid.put(uid, vis);
160                        }
161                    }
162                }
163            }
164        }
165        throw new IllegalStateException("Failed to reach END_DOCUMENT");
166    }
167
168    public void writeXml(XmlSerializer out) throws IOException {
169        out.startTag(null, TAG_RANKING);
170        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
171
172        final Set<String> packageNames = new ArraySet<>(mPackagePriorities.size()
173                + mPackageVisibilities.size());
174        packageNames.addAll(mPackagePriorities.keySet());
175        packageNames.addAll(mPackageVisibilities.keySet());
176        final Set<Integer> packageUids = new ArraySet<>();
177        for (String packageName : packageNames) {
178            packageUids.clear();
179            SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
180            SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
181            if (priorityByUid != null) {
182                final int M = priorityByUid.size();
183                for (int j = 0; j < M; j++) {
184                    packageUids.add(priorityByUid.keyAt(j));
185                }
186            }
187            if (visibilityByUid != null) {
188                final int M = visibilityByUid.size();
189                for (int j = 0; j < M; j++) {
190                    packageUids.add(visibilityByUid.keyAt(j));
191                }
192            }
193            for (Integer uid : packageUids) {
194                out.startTag(null, TAG_PACKAGE);
195                out.attribute(null, ATT_NAME, packageName);
196                if (priorityByUid != null) {
197                    final int priority = priorityByUid.get(uid);
198                    if (priority != Notification.PRIORITY_DEFAULT) {
199                        out.attribute(null, ATT_PRIORITY, Integer.toString(priority));
200                    }
201                }
202                if (visibilityByUid != null) {
203                    final int visibility = visibilityByUid.get(uid);
204                    if (visibility != NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE) {
205                        out.attribute(null, ATT_VISIBILITY, Integer.toString(visibility));
206                    }
207                }
208                out.attribute(null, ATT_UID, Integer.toString(uid));
209                out.endTag(null, TAG_PACKAGE);
210            }
211        }
212        out.endTag(null, TAG_RANKING);
213    }
214
215    private void updateConfig() {
216        final int N = mSignalExtractors.length;
217        for (int i = 0; i < N; i++) {
218            mSignalExtractors[i].setConfig(this);
219        }
220        mRankingHandler.sendEmptyMessage(NotificationManagerService.MESSAGE_RANKING_CONFIG_CHANGE);
221    }
222
223    public void sort(ArrayList<NotificationRecord> notificationList) {
224        final int N = notificationList.size();
225        // clear global sort keys
226        for (int i = N - 1; i >= 0; i--) {
227            notificationList.get(i).setGlobalSortKey(null);
228        }
229
230        try {
231            // rank each record individually
232            Collections.sort(notificationList, mPreliminaryComparator);
233        } catch (RuntimeException ex) {
234            // Don't crash the system server if something bad happened.
235            Log.e(TAG, "Extreme badness during notification sort", ex);
236            Log.e(TAG, "Current notification list: ");
237            for (int i = 0; i < N; i++) {
238                NotificationRecord nr = notificationList.get(i);
239                Log.e(TAG, String.format(
240                        "  [%d] %s (group %s, rank %d, sortkey %s)",
241                        i, nr, nr.getGroupKey(), nr.getAuthoritativeRank(),
242                        nr.getNotification().getSortKey()));
243            }
244            // STOPSHIP: remove once b/16626175 is found
245            throw ex;
246        }
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    @Override
315    public int getPackagePriority(String packageName, int uid) {
316        int priority = Notification.PRIORITY_DEFAULT;
317        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
318        if (priorityByUid != null) {
319            priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT);
320        }
321        return priority;
322    }
323
324    @Override
325    public void setPackagePriority(String packageName, int uid, int priority) {
326        if (priority == getPackagePriority(packageName, uid)) {
327            return;
328        }
329        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
330        if (priorityByUid == null) {
331            priorityByUid = new SparseIntArray();
332            mPackagePriorities.put(packageName, priorityByUid);
333        }
334        priorityByUid.put(uid, priority);
335        updateConfig();
336    }
337
338    @Override
339    public int getPackageVisibilityOverride(String packageName, int uid) {
340        int visibility = NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
341        SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
342        if (visibilityByUid != null) {
343            visibility = visibilityByUid.get(uid,
344                    NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
345        }
346        return visibility;
347    }
348
349    @Override
350    public void setPackageVisibilityOverride(String packageName, int uid, int visibility) {
351        if (visibility == getPackageVisibilityOverride(packageName, uid)) {
352            return;
353        }
354        SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
355        if (visibilityByUid == null) {
356            visibilityByUid = new SparseIntArray();
357            mPackageVisibilities.put(packageName, visibilityByUid);
358        }
359        visibilityByUid.put(uid, visibility);
360        updateConfig();
361    }
362
363    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
364        if (filter == null) {
365            final int N = mSignalExtractors.length;
366            pw.print(prefix);
367            pw.print("mSignalExtractors.length = ");
368            pw.println(N);
369            for (int i = 0; i < N; i++) {
370                pw.print(prefix);
371                pw.print("  ");
372                pw.println(mSignalExtractors[i]);
373            }
374        }
375        final int N = mPackagePriorities.size();
376        if (filter == null) {
377            pw.print(prefix);
378            pw.println("package priorities:");
379        }
380        for (int i = 0; i < N; i++) {
381            String name = mPackagePriorities.keyAt(i);
382            if (filter == null || filter.matches(name)) {
383                SparseIntArray priorityByUid = mPackagePriorities.get(name);
384                final int M = priorityByUid.size();
385                for (int j = 0; j < M; j++) {
386                    int uid = priorityByUid.keyAt(j);
387                    int priority = priorityByUid.get(uid);
388                    pw.print(prefix);
389                    pw.print("  ");
390                    pw.print(name);
391                    pw.print(" (");
392                    pw.print(uid);
393                    pw.print(") has priority: ");
394                    pw.println(priority);
395                }
396            }
397        }
398    }
399}
400