RankingHelper.java revision b5e44b796618c376cf535e43aaa86ea4522e7770
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                                mRestoredWithoutUids.put(name, r);
166                            }
167                        } else {
168                            r = getOrCreateRecord(name, uid);
169                        }
170                        r.importance = safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
171                        r.priority = safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY);
172                        r.visibility = safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
173
174                        final int innerDepth = parser.getDepth();
175                        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
176                                && (type != XmlPullParser.END_TAG
177                                || parser.getDepth() > innerDepth)) {
178                            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
179                                continue;
180                            }
181
182                            String tagName = parser.getName();
183                            if (TAG_CHANNEL.equals(tagName)) {
184                                String id = parser.getAttributeValue(null, ATT_ID);
185                                CharSequence channelName = parser.getAttributeValue(null, ATT_NAME);
186
187                                if (!TextUtils.isEmpty(id)) {
188                                    final NotificationChannel channel =
189                                            new NotificationChannel(id, channelName);
190                                    channel.populateFromXml(parser);
191                                    r.channels.put(id, channel);
192                                }
193                            }
194                        }
195                    }
196                }
197            }
198        }
199        throw new IllegalStateException("Failed to reach END_DOCUMENT");
200    }
201
202    private static String recordKey(String pkg, int uid) {
203        return pkg + "|" + uid;
204    }
205
206    private Record getOrCreateRecord(String pkg, int uid) {
207        final String key = recordKey(pkg, uid);
208        Record r = mRecords.get(key);
209        if (r == null) {
210            r = new Record();
211            r.pkg = pkg;
212            r.uid = uid;
213            NotificationChannel defaultChannel = createDefaultChannel();
214            r.channels.put(defaultChannel.getId(), defaultChannel);
215            mRecords.put(key, r);
216        }
217        return r;
218    }
219
220    private NotificationChannel createDefaultChannel() {
221        return new NotificationChannel(NotificationChannel.DEFAULT_CHANNEL_ID,
222                mContext.getString(R.string.default_notification_channel_label));
223    }
224
225    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
226        out.startTag(null, TAG_RANKING);
227        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
228
229        final int N = mRecords.size();
230        for (int i = 0; i < N; i++) {
231            final Record r = mRecords.valueAt(i);
232            //TODO: http://b/22388012
233            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
234                continue;
235            }
236            final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
237                    || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
238                    || r.channels.size() > 0;
239            if (hasNonDefaultSettings) {
240                out.startTag(null, TAG_PACKAGE);
241                out.attribute(null, ATT_NAME, r.pkg);
242                if (r.importance != DEFAULT_IMPORTANCE) {
243                    out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
244                }
245                if (r.priority != DEFAULT_PRIORITY) {
246                    out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
247                }
248                if (r.visibility != DEFAULT_VISIBILITY) {
249                    out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
250                }
251
252                if (!forBackup) {
253                    out.attribute(null, ATT_UID, Integer.toString(r.uid));
254                }
255
256                for (NotificationChannel channel : r.channels.values()) {
257                    channel.writeXml(out);
258                }
259
260                out.endTag(null, TAG_PACKAGE);
261            }
262        }
263        out.endTag(null, TAG_RANKING);
264    }
265
266    private void updateConfig() {
267        final int N = mSignalExtractors.length;
268        for (int i = 0; i < N; i++) {
269            mSignalExtractors[i].setConfig(this);
270        }
271        mRankingHandler.requestSort();
272    }
273
274    public void sort(ArrayList<NotificationRecord> notificationList) {
275        final int N = notificationList.size();
276        // clear global sort keys
277        for (int i = N - 1; i >= 0; i--) {
278            notificationList.get(i).setGlobalSortKey(null);
279        }
280
281        // rank each record individually
282        Collections.sort(notificationList, mPreliminaryComparator);
283
284        synchronized (mProxyByGroupTmp) {
285            // record individual ranking result and nominate proxies for each group
286            for (int i = N - 1; i >= 0; i--) {
287                final NotificationRecord record = notificationList.get(i);
288                record.setAuthoritativeRank(i);
289                final String groupKey = record.getGroupKey();
290                boolean isGroupSummary = record.getNotification().isGroupSummary();
291                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
292                    mProxyByGroupTmp.put(groupKey, record);
293                }
294            }
295            // assign global sort key:
296            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
297            for (int i = 0; i < N; i++) {
298                final NotificationRecord record = notificationList.get(i);
299                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
300                String groupSortKey = record.getNotification().getSortKey();
301
302                // We need to make sure the developer provided group sort key (gsk) is handled
303                // correctly:
304                //   gsk="" < gsk=non-null-string < gsk=null
305                //
306                // We enforce this by using different prefixes for these three cases.
307                String groupSortKeyPortion;
308                if (groupSortKey == null) {
309                    groupSortKeyPortion = "nsk";
310                } else if (groupSortKey.equals("")) {
311                    groupSortKeyPortion = "esk";
312                } else {
313                    groupSortKeyPortion = "gsk=" + groupSortKey;
314                }
315
316                boolean isGroupSummary = record.getNotification().isGroupSummary();
317                record.setGlobalSortKey(
318                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
319                        record.isRecentlyIntrusive() ? '0' : '1',
320                        groupProxy.getAuthoritativeRank(),
321                        isGroupSummary ? '0' : '1',
322                        groupSortKeyPortion,
323                        record.getAuthoritativeRank()));
324            }
325            mProxyByGroupTmp.clear();
326        }
327
328        // Do a second ranking pass, using group proxies
329        Collections.sort(notificationList, mFinalComparator);
330    }
331
332    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
333        return Collections.binarySearch(notificationList, target, mFinalComparator);
334    }
335
336    private static int safeInt(XmlPullParser parser, String att, int defValue) {
337        final String val = parser.getAttributeValue(null, att);
338        return tryParseInt(val, defValue);
339    }
340
341    private static int tryParseInt(String value, int defValue) {
342        if (TextUtils.isEmpty(value)) return defValue;
343        try {
344            return Integer.parseInt(value);
345        } catch (NumberFormatException e) {
346            return defValue;
347        }
348    }
349
350    /**
351     * Gets priority.
352     */
353    @Override
354    public int getPriority(String packageName, int uid) {
355        return getOrCreateRecord(packageName, uid).priority;
356    }
357
358    /**
359     * Sets priority.
360     */
361    @Override
362    public void setPriority(String packageName, int uid, int priority) {
363        getOrCreateRecord(packageName, uid).priority = priority;
364        updateConfig();
365    }
366
367    /**
368     * Gets visual override.
369     */
370    @Override
371    public int getVisibilityOverride(String packageName, int uid) {
372        return getOrCreateRecord(packageName, uid).visibility;
373    }
374
375    /**
376     * Sets visibility override.
377     */
378    @Override
379    public void setVisibilityOverride(String pkgName, int uid, int visibility) {
380        getOrCreateRecord(pkgName, uid).visibility = visibility;
381        updateConfig();
382    }
383
384    /**
385     * Gets importance.
386     */
387    @Override
388    public int getImportance(String packageName, int uid) {
389        return getOrCreateRecord(packageName, uid).importance;
390    }
391
392    @Override
393    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel) {
394        Record r = getOrCreateRecord(pkg, uid);
395        if (r.channels.containsKey(channel.getId()) || channel.getName().equals(
396                mContext.getString(R.string.default_notification_channel_label))) {
397            throw new IllegalArgumentException("Channel already exists");
398        }
399        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
400            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
401        }
402        r.channels.put(channel.getId(), channel);
403        updateConfig();
404    }
405
406    @Override
407    public void updateNotificationChannel(int callingUid, String pkg, int uid,
408            NotificationChannel updatedChannel) {
409        Record r = getOrCreateRecord(pkg, uid);
410        NotificationChannel channel = r.channels.get(updatedChannel.getId());
411        if (channel == null) {
412            throw new IllegalArgumentException("Channel does not exist");
413        }
414        if (!isUidSystem(callingUid)) {
415            updatedChannel.setImportance(channel.getImportance());
416            updatedChannel.setName(channel.getName());
417        }
418        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
419            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
420        }
421        r.channels.put(updatedChannel.getId(), updatedChannel);
422        updateConfig();
423    }
424
425    @Override
426    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId) {
427        Record r = getOrCreateRecord(pkg, uid);
428        if (channelId == null) {
429            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
430        }
431        return r.channels.get(channelId);
432    }
433
434    @Override
435    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
436        Record r = getOrCreateRecord(pkg, uid);
437        r.channels.remove(channelId);
438    }
439
440    @Override
441    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid) {
442        List<NotificationChannel> channels = new ArrayList<>();
443        Record r = getOrCreateRecord(pkg, uid);
444        int N = r.channels.size();
445        for (int i = 0; i < N; i++) {
446            channels.add(r.channels.valueAt(i));
447        }
448        return new ParceledListSlice<NotificationChannel>(channels);
449    }
450
451    /**
452     * Sets importance.
453     */
454    @Override
455    public void setImportance(String pkgName, int uid, int importance) {
456        getOrCreateRecord(pkgName, uid).importance = importance;
457        updateConfig();
458    }
459
460    public void setEnabled(String packageName, int uid, boolean enabled) {
461        boolean wasEnabled = getImportance(packageName, uid) != Ranking.IMPORTANCE_NONE;
462        if (wasEnabled == enabled) {
463            return;
464        }
465        setImportance(packageName, uid, enabled ? DEFAULT_IMPORTANCE : Ranking.IMPORTANCE_NONE);
466    }
467
468    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
469        if (filter == null) {
470            final int N = mSignalExtractors.length;
471            pw.print(prefix);
472            pw.print("mSignalExtractors.length = ");
473            pw.println(N);
474            for (int i = 0; i < N; i++) {
475                pw.print(prefix);
476                pw.print("  ");
477                pw.println(mSignalExtractors[i]);
478            }
479        }
480        if (filter == null) {
481            pw.print(prefix);
482            pw.println("per-package config:");
483        }
484        pw.println("Records:");
485        dumpRecords(pw, prefix, filter, mRecords);
486        pw.println("Restored without uid:");
487        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
488    }
489
490    private static void dumpRecords(PrintWriter pw, String prefix,
491            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
492        final int N = records.size();
493        for (int i = 0; i < N; i++) {
494            final Record r = records.valueAt(i);
495            if (filter == null || filter.matches(r.pkg)) {
496                pw.print(prefix);
497                pw.print("  ");
498                pw.print(r.pkg);
499                pw.print(" (");
500                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
501                pw.print(')');
502                if (r.importance != DEFAULT_IMPORTANCE) {
503                    pw.print(" importance=");
504                    pw.print(Ranking.importanceToString(r.importance));
505                }
506                if (r.priority != DEFAULT_PRIORITY) {
507                    pw.print(" priority=");
508                    pw.print(Notification.priorityToString(r.priority));
509                }
510                if (r.visibility != DEFAULT_VISIBILITY) {
511                    pw.print(" visibility=");
512                    pw.print(Notification.visibilityToString(r.visibility));
513                }
514                pw.println();
515                for (NotificationChannel channel : r.channels.values()) {
516                    pw.print(prefix);
517                    pw.print("  ");
518                    pw.print("  ");
519                    pw.println(channel);
520                }
521            }
522        }
523    }
524
525    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
526        JSONObject ranking = new JSONObject();
527        JSONArray records = new JSONArray();
528        try {
529            ranking.put("noUid", mRestoredWithoutUids.size());
530        } catch (JSONException e) {
531           // pass
532        }
533        final int N = mRecords.size();
534        for (int i = 0; i < N; i++) {
535            final Record r = mRecords.valueAt(i);
536            if (filter == null || filter.matches(r.pkg)) {
537                JSONObject record = new JSONObject();
538                try {
539                    record.put("userId", UserHandle.getUserId(r.uid));
540                    record.put("packageName", r.pkg);
541                    if (r.importance != DEFAULT_IMPORTANCE) {
542                        record.put("importance", Ranking.importanceToString(r.importance));
543                    }
544                    if (r.priority != DEFAULT_PRIORITY) {
545                        record.put("priority", Notification.priorityToString(r.priority));
546                    }
547                    if (r.visibility != DEFAULT_VISIBILITY) {
548                        record.put("visibility", Notification.visibilityToString(r.visibility));
549                    }
550                    for (NotificationChannel channel : r.channels.values()) {
551                        record.put("channel", channel.toJson());
552                    }
553                } catch (JSONException e) {
554                   // pass
555                }
556                records.put(record);
557            }
558        }
559        try {
560            ranking.put("records", records);
561        } catch (JSONException e) {
562            // pass
563        }
564        return ranking;
565    }
566
567    /**
568     * Dump only the ban information as structured JSON for the stats collector.
569     *
570     * This is intentionally redundant with {#link dumpJson} because the old
571     * scraper will expect this format.
572     *
573     * @param filter
574     * @return
575     */
576    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
577        JSONArray bans = new JSONArray();
578        Map<Integer, String> packageBans = getPackageBans();
579        for(Entry<Integer, String> ban : packageBans.entrySet()) {
580            final int userId = UserHandle.getUserId(ban.getKey());
581            final String packageName = ban.getValue();
582            if (filter == null || filter.matches(packageName)) {
583                JSONObject banJson = new JSONObject();
584                try {
585                    banJson.put("userId", userId);
586                    banJson.put("packageName", packageName);
587                } catch (JSONException e) {
588                    e.printStackTrace();
589                }
590                bans.put(banJson);
591            }
592        }
593        return bans;
594    }
595
596    public Map<Integer, String> getPackageBans() {
597        final int N = mRecords.size();
598        ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
599        for (int i = 0; i < N; i++) {
600            final Record r = mRecords.valueAt(i);
601            if (r.importance == Ranking.IMPORTANCE_NONE) {
602                packageBans.put(r.uid, r.pkg);
603            }
604        }
605        return packageBans;
606    }
607
608    public void onPackagesChanged(boolean removingPackage, String[] pkgList) {
609        if (!removingPackage || pkgList == null || pkgList.length == 0
610                || mRestoredWithoutUids.isEmpty()) {
611            return; // nothing to do
612        }
613        final PackageManager pm = mContext.getPackageManager();
614        boolean updated = false;
615        for (String pkg : pkgList) {
616            final Record r = mRestoredWithoutUids.get(pkg);
617            if (r != null) {
618                try {
619                    //TODO: http://b/22388012
620                    r.uid = pm.getPackageUidAsUser(r.pkg, UserHandle.USER_SYSTEM);
621                    mRestoredWithoutUids.remove(pkg);
622                    mRecords.put(recordKey(r.pkg, r.uid), r);
623                    updated = true;
624                } catch (NameNotFoundException e) {
625                    // noop
626                }
627            }
628        }
629        if (updated) {
630            updateConfig();
631        }
632    }
633
634    private static boolean isUidSystem(int uid) {
635        final int appid = UserHandle.getAppId(uid);
636        return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
637    }
638
639    private static class Record {
640        static int UNKNOWN_UID = UserHandle.USER_NULL;
641
642        String pkg;
643        int uid = UNKNOWN_UID;
644        int importance = DEFAULT_IMPORTANCE;
645        int priority = DEFAULT_PRIORITY;
646        int visibility = DEFAULT_VISIBILITY;
647
648        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
649   }
650}
651