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        // rank each record individually
231        Collections.sort(notificationList, mPreliminaryComparator);
232
233        synchronized (mProxyByGroupTmp) {
234            // record individual ranking result and nominate proxies for each group
235            for (int i = N - 1; i >= 0; i--) {
236                final NotificationRecord record = notificationList.get(i);
237                record.setAuthoritativeRank(i);
238                final String groupKey = record.getGroupKey();
239                boolean isGroupSummary = record.getNotification().isGroupSummary();
240                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
241                    mProxyByGroupTmp.put(groupKey, record);
242                }
243            }
244            // assign global sort key:
245            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
246            for (int i = 0; i < N; i++) {
247                final NotificationRecord record = notificationList.get(i);
248                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
249                String groupSortKey = record.getNotification().getSortKey();
250
251                // We need to make sure the developer provided group sort key (gsk) is handled
252                // correctly:
253                //   gsk="" < gsk=non-null-string < gsk=null
254                //
255                // We enforce this by using different prefixes for these three cases.
256                String groupSortKeyPortion;
257                if (groupSortKey == null) {
258                    groupSortKeyPortion = "nsk";
259                } else if (groupSortKey.equals("")) {
260                    groupSortKeyPortion = "esk";
261                } else {
262                    groupSortKeyPortion = "gsk=" + groupSortKey;
263                }
264
265                boolean isGroupSummary = record.getNotification().isGroupSummary();
266                record.setGlobalSortKey(
267                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
268                        record.isRecentlyIntrusive() ? '0' : '1',
269                        groupProxy.getAuthoritativeRank(),
270                        isGroupSummary ? '0' : '1',
271                        groupSortKeyPortion,
272                        record.getAuthoritativeRank()));
273            }
274            mProxyByGroupTmp.clear();
275        }
276
277        // Do a second ranking pass, using group proxies
278        Collections.sort(notificationList, mFinalComparator);
279    }
280
281    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
282        return Collections.binarySearch(notificationList, target, mFinalComparator);
283    }
284
285    private static int safeInt(XmlPullParser parser, String att, int defValue) {
286        final String val = parser.getAttributeValue(null, att);
287        return tryParseInt(val, defValue);
288    }
289
290    private static int tryParseInt(String value, int defValue) {
291        if (TextUtils.isEmpty(value)) return defValue;
292        try {
293            return Integer.valueOf(value);
294        } catch (NumberFormatException e) {
295            return defValue;
296        }
297    }
298
299    @Override
300    public int getPackagePriority(String packageName, int uid) {
301        int priority = Notification.PRIORITY_DEFAULT;
302        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
303        if (priorityByUid != null) {
304            priority = priorityByUid.get(uid, Notification.PRIORITY_DEFAULT);
305        }
306        return 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        SparseIntArray priorityByUid = mPackagePriorities.get(packageName);
315        if (priorityByUid == null) {
316            priorityByUid = new SparseIntArray();
317            mPackagePriorities.put(packageName, priorityByUid);
318        }
319        priorityByUid.put(uid, priority);
320        updateConfig();
321    }
322
323    @Override
324    public int getPackageVisibilityOverride(String packageName, int uid) {
325        int visibility = NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE;
326        SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
327        if (visibilityByUid != null) {
328            visibility = visibilityByUid.get(uid,
329                    NotificationListenerService.Ranking.VISIBILITY_NO_OVERRIDE);
330        }
331        return visibility;
332    }
333
334    @Override
335    public void setPackageVisibilityOverride(String packageName, int uid, int visibility) {
336        if (visibility == getPackageVisibilityOverride(packageName, uid)) {
337            return;
338        }
339        SparseIntArray visibilityByUid = mPackageVisibilities.get(packageName);
340        if (visibilityByUid == null) {
341            visibilityByUid = new SparseIntArray();
342            mPackageVisibilities.put(packageName, visibilityByUid);
343        }
344        visibilityByUid.put(uid, visibility);
345        updateConfig();
346    }
347
348    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
349        if (filter == null) {
350            final int N = mSignalExtractors.length;
351            pw.print(prefix);
352            pw.print("mSignalExtractors.length = ");
353            pw.println(N);
354            for (int i = 0; i < N; i++) {
355                pw.print(prefix);
356                pw.print("  ");
357                pw.println(mSignalExtractors[i]);
358            }
359        }
360        final int N = mPackagePriorities.size();
361        if (filter == null) {
362            pw.print(prefix);
363            pw.println("package priorities:");
364        }
365        for (int i = 0; i < N; i++) {
366            String name = mPackagePriorities.keyAt(i);
367            if (filter == null || filter.matches(name)) {
368                SparseIntArray priorityByUid = mPackagePriorities.get(name);
369                final int M = priorityByUid.size();
370                for (int j = 0; j < M; j++) {
371                    int uid = priorityByUid.keyAt(j);
372                    int priority = priorityByUid.get(uid);
373                    pw.print(prefix);
374                    pw.print("  ");
375                    pw.print(name);
376                    pw.print(" (");
377                    pw.print(uid);
378                    pw.print(") has priority: ");
379                    pw.println(priority);
380                }
381            }
382        }
383    }
384}
385