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