RankingHelper.java revision 808913e008b1038d7f6ad5442ae203ada7290a81
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.app.NotificationChannel;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.content.pm.PackageManager.NameNotFoundException;
23import android.content.pm.ParceledListSlice;
24import android.os.Process;
25import android.os.UserHandle;
26import android.service.notification.NotificationListenerService.Ranking;
27import android.text.TextUtils;
28import android.util.ArrayMap;
29import android.util.Slog;
30
31import com.android.internal.R;
32
33import org.json.JSONArray;
34import org.json.JSONException;
35import org.json.JSONObject;
36import org.xmlpull.v1.XmlPullParser;
37import org.xmlpull.v1.XmlPullParserException;
38import org.xmlpull.v1.XmlSerializer;
39
40import java.io.IOException;
41import java.io.PrintWriter;
42import java.util.ArrayList;
43import java.util.Collections;
44import java.util.List;
45import java.util.Map;
46import java.util.Map.Entry;
47
48public class RankingHelper implements RankingConfig {
49    private static final String TAG = "RankingHelper";
50
51    private static final int XML_VERSION = 1;
52
53    private static final String TAG_RANKING = "ranking";
54    private static final String TAG_PACKAGE = "package";
55    private static final String TAG_CHANNEL = "channel";
56
57    private static final String ATT_VERSION = "version";
58    private static final String ATT_NAME = "name";
59    private static final String ATT_UID = "uid";
60    private static final String ATT_ID = "id";
61    private static final String ATT_PRIORITY = "priority";
62    private static final String ATT_VISIBILITY = "visibility";
63    private static final String ATT_IMPORTANCE = "importance";
64
65    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
66    private static final int DEFAULT_VISIBILITY = Ranking.VISIBILITY_NO_OVERRIDE;
67    private static final int DEFAULT_IMPORTANCE = Ranking.IMPORTANCE_UNSPECIFIED;
68
69    private final NotificationSignalExtractor[] mSignalExtractors;
70    private final NotificationComparator mPreliminaryComparator = new NotificationComparator();
71    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
72
73    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
74    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
75    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
76
77    private final Context mContext;
78    private final RankingHandler mRankingHandler;
79
80    public RankingHelper(Context context, RankingHandler rankingHandler,
81            NotificationUsageStats usageStats, String[] extractorNames) {
82        mContext = context;
83        mRankingHandler = rankingHandler;
84
85        final int N = extractorNames.length;
86        mSignalExtractors = new NotificationSignalExtractor[N];
87        for (int i = 0; i < N; i++) {
88            try {
89                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
90                NotificationSignalExtractor extractor =
91                        (NotificationSignalExtractor) extractorClass.newInstance();
92                extractor.initialize(mContext, usageStats);
93                extractor.setConfig(this);
94                mSignalExtractors[i] = extractor;
95            } catch (ClassNotFoundException e) {
96                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
97            } catch (InstantiationException e) {
98                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
99            } catch (IllegalAccessException e) {
100                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
101            }
102        }
103    }
104
105    @SuppressWarnings("unchecked")
106    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
107        final int N = mSignalExtractors.length;
108        for (int i = 0; i < N; i++) {
109            final NotificationSignalExtractor extractor = mSignalExtractors[i];
110            if (extractorClass.equals(extractor.getClass())) {
111                return (T) extractor;
112            }
113        }
114        return null;
115    }
116
117    public void extractSignals(NotificationRecord r) {
118        final int N = mSignalExtractors.length;
119        for (int i = 0; i < N; i++) {
120            NotificationSignalExtractor extractor = mSignalExtractors[i];
121            try {
122                RankingReconsideration recon = extractor.process(r);
123                if (recon != null) {
124                    mRankingHandler.requestReconsideration(recon);
125                }
126            } catch (Throwable t) {
127                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
128            }
129        }
130    }
131
132    public void readXml(XmlPullParser parser, boolean forRestore)
133            throws XmlPullParserException, IOException {
134        final PackageManager pm = mContext.getPackageManager();
135        int type = parser.getEventType();
136        if (type != XmlPullParser.START_TAG) return;
137        String tag = parser.getName();
138        if (!TAG_RANKING.equals(tag)) return;
139        mRecords.clear();
140        mRestoredWithoutUids.clear();
141        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
142            tag = parser.getName();
143            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
144                return;
145            }
146            if (type == XmlPullParser.START_TAG) {
147                if (TAG_PACKAGE.equals(tag)) {
148                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
149                    String name = parser.getAttributeValue(null, ATT_NAME);
150
151                    if (!TextUtils.isEmpty(name)) {
152                        if (forRestore) {
153                            try {
154                                //TODO: http://b/22388012
155                                uid = pm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
156                            } catch (NameNotFoundException e) {
157                                // noop
158                            }
159                        }
160                        Record r = null;
161                        if (uid == Record.UNKNOWN_UID) {
162                            r = mRestoredWithoutUids.get(name);
163                            if (r == null) {
164                                r = new Record();
165                                r.pkg = name;
166                                mRestoredWithoutUids.put(name, r);
167                            }
168                        } else {
169                            r = getOrCreateRecord(name, uid);
170                        }
171                        r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
172                        r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
173                        r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
174
175                        final int innerDepth = parser.getDepth();
176                        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
177                                && (type != XmlPullParser.END_TAG
178                                || parser.getDepth() > innerDepth)) {
179                            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
180                                continue;
181                            }
182
183                            String tagName = parser.getName();
184                            if (TAG_CHANNEL.equals(tagName)) {
185                                String id = parser.getAttributeValue(null, ATT_ID);
186                                CharSequence channelName = parser.getAttributeValue(null, ATT_NAME);
187
188                                if (!TextUtils.isEmpty(id)) {
189                                    final NotificationChannel channel =
190                                            new NotificationChannel(id, channelName);
191                                    channel.populateFromXml(parser);
192                                    r.channels.put(id, channel);
193                                }
194                            }
195                        }
196                    }
197                }
198            }
199        }
200        throw new IllegalStateException("Failed to reach END_DOCUMENT");
201    }
202
203    private static String recordKey(String pkg, int uid) {
204        return pkg + "|" + uid;
205    }
206
207    private Record getOrCreateRecord(String pkg, int uid) {
208        final String key = recordKey(pkg, uid);
209        Record r = mRecords.get(key);
210        if (r == null) {
211            r = new Record();
212            r.pkg = pkg;
213            r.uid = uid;
214            NotificationChannel defaultChannel = createDefaultChannel();
215            r.channels.put(defaultChannel.getId(), defaultChannel);
216            mRecords.put(key, r);
217        }
218        return r;
219    }
220
221    private NotificationChannel createDefaultChannel() {
222        return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID,
223                mContext.getString(R.string.default_notification_channel_label));
224    }
225
226    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
227        out.startTag(null, TAG_RANKING);
228        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
229
230        final int N = mRecords.size();
231        for (int i = 0; i < N; i++) {
232            final Record r = mRecords.valueAt(i);
233            //TODO: http://b/22388012
234            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
235                continue;
236            }
237            final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
238                    || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
239                    || r.channels.size() > 0;
240            if (hasNonDefaultSettings) {
241                out.startTag(null, TAG_PACKAGE);
242                out.attribute(null, ATT_NAME, r.pkg);
243                if (r.importance != DEFAULT_IMPORTANCE) {
244                    out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
245                }
246                if (r.priority != DEFAULT_PRIORITY) {
247                    out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
248                }
249                if (r.visibility != DEFAULT_VISIBILITY) {
250                    out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
251                }
252
253                if (!forBackup) {
254                    out.attribute(null, ATT_UID, Integer.toString(r.uid));
255                }
256
257                for (NotificationChannel channel : r.channels.values()) {
258                    channel.writeXml(out);
259                }
260
261                out.endTag(null, TAG_PACKAGE);
262            }
263        }
264        out.endTag(null, TAG_RANKING);
265    }
266
267    private void updateConfig() {
268        final int N = mSignalExtractors.length;
269        for (int i = 0; i < N; i++) {
270            mSignalExtractors[i].setConfig(this);
271        }
272        mRankingHandler.requestSort();
273    }
274
275    public void sort(ArrayList<NotificationRecord> notificationList) {
276        final int N = notificationList.size();
277        // clear global sort keys
278        for (int i = N - 1; i >= 0; i--) {
279            notificationList.get(i).setGlobalSortKey(null);
280        }
281
282        // rank each record individually
283        Collections.sort(notificationList, mPreliminaryComparator);
284
285        synchronized (mProxyByGroupTmp) {
286            // record individual ranking result and nominate proxies for each group
287            for (int i = N - 1; i >= 0; i--) {
288                final NotificationRecord record = notificationList.get(i);
289                record.setAuthoritativeRank(i);
290                final String groupKey = record.getGroupKey();
291                boolean isGroupSummary = record.getNotification().isGroupSummary();
292                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
293                    mProxyByGroupTmp.put(groupKey, record);
294                }
295            }
296            // assign global sort key:
297            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
298            for (int i = 0; i < N; i++) {
299                final NotificationRecord record = notificationList.get(i);
300                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
301                String groupSortKey = record.getNotification().getSortKey();
302
303                // We need to make sure the developer provided group sort key (gsk) is handled
304                // correctly:
305                //   gsk="" < gsk=non-null-string < gsk=null
306                //
307                // We enforce this by using different prefixes for these three cases.
308                String groupSortKeyPortion;
309                if (groupSortKey == null) {
310                    groupSortKeyPortion = "nsk";
311                } else if (groupSortKey.equals("")) {
312                    groupSortKeyPortion = "esk";
313                } else {
314                    groupSortKeyPortion = "gsk=" + groupSortKey;
315                }
316
317                boolean isGroupSummary = record.getNotification().isGroupSummary();
318                record.setGlobalSortKey(
319                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
320                        record.isRecentlyIntrusive() ? '0' : '1',
321                        groupProxy.getAuthoritativeRank(),
322                        isGroupSummary ? '0' : '1',
323                        groupSortKeyPortion,
324                        record.getAuthoritativeRank()));
325            }
326            mProxyByGroupTmp.clear();
327        }
328
329        // Do a second ranking pass, using group proxies
330        Collections.sort(notificationList, mFinalComparator);
331    }
332
333    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
334        return Collections.binarySearch(notificationList, target, mFinalComparator);
335    }
336
337    private static int safeInt(XmlPullParser parser, String att, int defValue) {
338        final String val = parser.getAttributeValue(null, att);
339        return tryParseInt(val, defValue);
340    }
341
342    private static int tryParseInt(String value, int defValue) {
343        if (TextUtils.isEmpty(value)) return defValue;
344        try {
345            return Integer.parseInt(value);
346        } catch (NumberFormatException e) {
347            return defValue;
348        }
349    }
350
351    /**
352     * Gets priority.
353     */
354    @Override
355    public int getPriority(String packageName, int uid) {
356        return getOrCreateRecord(packageName, uid).priority;
357    }
358
359    /**
360     * Sets priority.
361     */
362    @Override
363    public void setPriority(String packageName, int uid, int priority) {
364        getOrCreateRecord(packageName, uid).priority = priority;
365        updateConfig();
366    }
367
368    /**
369     * Gets visual override.
370     */
371    @Override
372    public int getVisibilityOverride(String packageName, int uid) {
373        return getOrCreateRecord(packageName, uid).visibility;
374    }
375
376    /**
377     * Sets visibility override.
378     */
379    @Override
380    public void setVisibilityOverride(String pkgName, int uid, int visibility) {
381        getOrCreateRecord(pkgName, uid).visibility = visibility;
382        updateConfig();
383    }
384
385    /**
386     * Gets importance.
387     */
388    @Override
389    public int getImportance(String packageName, int uid) {
390        return getOrCreateRecord(packageName, uid).importance;
391    }
392
393    @Override
394    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel) {
395        Record r = getOrCreateRecord(pkg, uid);
396        if (r.channels.containsKey(channel.getId()) || channel.getName().equals(
397                mContext.getString(R.string.default_notification_channel_label))) {
398            throw new IllegalArgumentException("Channel already exists");
399        }
400        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
401            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
402        }
403        r.channels.put(channel.getId(), channel);
404        updateConfig();
405    }
406
407    @Override
408    public void updateNotificationChannel(int callingUid, String pkg, int uid,
409            NotificationChannel updatedChannel) {
410        Record r = getOrCreateRecord(pkg, uid);
411        NotificationChannel channel = r.channels.get(updatedChannel.getId());
412        if (channel == null) {
413            throw new IllegalArgumentException("Channel does not exist");
414        }
415        if (!isUidSystem(callingUid)) {
416            updatedChannel.setImportance(channel.getImportance());
417            updatedChannel.setName(channel.getName());
418        }
419        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
420            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
421        }
422        r.channels.put(updatedChannel.getId(), updatedChannel);
423        updateConfig();
424    }
425
426    @Override
427    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId) {
428        Record r = getOrCreateRecord(pkg, uid);
429        if (channelId == null) {
430            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
431        }
432        return r.channels.get(channelId);
433    }
434
435    @Override
436    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
437        Record r = getOrCreateRecord(pkg, uid);
438        r.channels.remove(channelId);
439    }
440
441    @Override
442    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid) {
443        List<NotificationChannel> channels = new ArrayList<>();
444        Record r = getOrCreateRecord(pkg, uid);
445        int N = r.channels.size();
446        for (int i = 0; i < N; i++) {
447            channels.add(r.channels.valueAt(i));
448        }
449        return new ParceledListSlice<NotificationChannel>(channels);
450    }
451
452    /**
453     * Sets importance.
454     */
455    @Override
456    public void setImportance(String pkgName, int uid, int importance) {
457        getOrCreateRecord(pkgName, uid).importance = importance;
458        updateConfig();
459    }
460
461    public void setEnabled(String packageName, int uid, boolean enabled) {
462        boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE;
463        if (wasEnabled == enabled) {
464            return;
465        }
466        setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE);
467    }
468
469    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
470        if (filter == null) {
471            final int N = mSignalExtractors.length;
472            pw.print(prefix);
473            pw.print("mSignalExtractors.length = ");
474            pw.println(N);
475            for (int i = 0; i < N; i++) {
476                pw.print(prefix);
477                pw.print("  ");
478                pw.println(mSignalExtractors[i]);
479            }
480        }
481        if (filter == null) {
482            pw.print(prefix);
483            pw.println("per-package config:");
484        }
485        pw.println("Records:");
486        dumpRecords(pw, prefix, filter, mRecords);
487        pw.println("Restored without uid:");
488        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
489    }
490
491    private static void dumpRecords(PrintWriter pw, String prefix,
492            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
493        final int N = records.size();
494        for (int i = 0; i < N; i++) {
495            final Record r = records.valueAt(i);
496            if (filter == null || filter.matches(r.pkg)) {
497                pw.print(prefix);
498                pw.print("  ");
499                pw.print(r.pkg);
500                pw.print(" (");
501                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
502                pw.print(')');
503                if (r.importance != DEFAULT_IMPORTANCE) {
504                    pw.print(" importance=");
505                    pw.print(Ranking.importanceToString(r.importance));
506                }
507                if (r.priority != DEFAULT_PRIORITY) {
508                    pw.print(" priority=");
509                    pw.print(Notification.priorityToString(r.priority));
510                }
511                if (r.visibility != DEFAULT_VISIBILITY) {
512                    pw.print(" visibility=");
513                    pw.print(Notification.visibilityToString(r.visibility));
514                }
515                pw.println();
516                for (NotificationChannel channel : r.channels.values()) {
517                    pw.print(prefix);
518                    pw.print("  ");
519                    pw.print("  ");
520                    pw.println(channel);
521                }
522            }
523        }
524    }
525
526    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
527        JSONObject ranking = new JSONObject();
528        JSONArray records = new JSONArray();
529        try {
530            ranking.put("noUid", mRestoredWithoutUids.size());
531        } catch (JSONException e) {
532           // pass
533        }
534        final int N = mRecords.size();
535        for (int i = 0; i < N; i++) {
536            final Record r = mRecords.valueAt(i);
537            if (filter == null || filter.matches(r.pkg)) {
538                JSONObject record = new JSONObject();
539                try {
540                    record.put("userId", UserHandle.getUserId(r.uid));
541                    record.put("packageName", r.pkg);
542                    if (r.importance != DEFAULT_IMPORTANCE) {
543                        record.put("importance", Ranking.importanceToString(r.importance));
544                    }
545                    if (r.priority != DEFAULT_PRIORITY) {
546                        record.put("priority", Notification.priorityToString(r.priority));
547                    }
548                    if (r.visibility != DEFAULT_VISIBILITY) {
549                        record.put("visibility", Notification.visibilityToString(r.visibility));
550                    }
551                    for (NotificationChannel channel : r.channels.values()) {
552                        record.put("channel", channel.toJson());
553                    }
554                } catch (JSONException e) {
555                   // pass
556                }
557                records.put(record);
558            }
559        }
560        try {
561            ranking.put("records", records);
562        } catch (JSONException e) {
563            // pass
564        }
565        return ranking;
566    }
567
568    /**
569     * Dump only the ban information as structured JSON for the stats collector.
570     *
571     * This is intentionally redundant with {#link dumpJson} because the old
572     * scraper will expect this format.
573     *
574     * @param filter
575     * @return
576     */
577    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
578        JSONArray bans = new JSONArray();
579        Map<Integer, String> packageBans = getPackageBans();
580        for(Entry<Integer, String> ban : packageBans.entrySet()) {
581            final int userId = UserHandle.getUserId(ban.getKey());
582            final String packageName = ban.getValue();
583            if (filter == null || filter.matches(packageName)) {
584                JSONObject banJson = new JSONObject();
585                try {
586                    banJson.put("userId", userId);
587                    banJson.put("packageName", packageName);
588                } catch (JSONException e) {
589                    e.printStackTrace();
590                }
591                bans.put(banJson);
592            }
593        }
594        return bans;
595    }
596
597    public Map<Integer, String> getPackageBans() {
598        final int N = mRecords.size();
599        ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
600        for (int i = 0; i < N; i++) {
601            final Record r = mRecords.valueAt(i);
602            if (r.importance == Ranking.IMPORTANCE_NONE) {
603                packageBans.put(r.uid, r.pkg);
604            }
605        }
606        return packageBans;
607    }
608
609    public void onPackagesChanged(boolean removingPackage, String[] pkgList) {
610        if (removingPackage || pkgList == null || pkgList.length == 0
611                || mRestoredWithoutUids.isEmpty()) {
612            return; // nothing to do
613        }
614        final PackageManager pm = mContext.getPackageManager();
615        boolean updated = false;
616        for (String pkg : pkgList) {
617            final Record r = mRestoredWithoutUids.get(pkg);
618            if (r != null) {
619                try {
620                    //TODO: http://b/22388012
621                    r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
622                    mRestoredWithoutUids.remove(pkg);
623                    mRecords.put(recordKey(r.pkg, r.uid), r);
624                    updated = true;
625                } catch (NameNotFoundException e) {
626                    // noop
627                }
628            }
629        }
630        if (updated) {
631            updateConfig();
632        }
633    }
634
635    private static boolean isUidSystem(int uid) {
636        final int appid = UserHandle.getAppId(uid);
637        return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
638    }
639
640    private static class Record {
641        static int UNKNOWN_UID = UserHandle.USER_NULL;
642
643        String pkg;
644        int uid = UNKNOWN_UID;
645        int importance = DEFAULT_IMPORTANCE;
646        int priority = DEFAULT_PRIORITY;
647        int visibility = DEFAULT_VISIBILITY;
648
649        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
650   }
651}
652