RankingHelper.java revision 4036e8d4c636ae36f28585d283b522a7a97eaf72
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 static android.app.NotificationManager.IMPORTANCE_NONE;
19
20import com.android.internal.R;
21import com.android.internal.annotations.VisibleForTesting;
22import com.android.internal.util.Preconditions;
23
24import android.app.Notification;
25import android.app.NotificationChannel;
26import android.app.NotificationManager;
27import android.content.Context;
28import android.content.pm.ApplicationInfo;
29import android.content.pm.PackageManager;
30import android.content.pm.PackageManager.NameNotFoundException;
31import android.content.pm.ParceledListSlice;
32import android.os.Build;
33import android.os.UserHandle;
34import android.service.notification.NotificationListenerService.Ranking;
35import android.text.TextUtils;
36import android.util.ArrayMap;
37import android.util.Slog;
38
39import org.json.JSONArray;
40import org.json.JSONException;
41import org.json.JSONObject;
42import org.xmlpull.v1.XmlPullParser;
43import org.xmlpull.v1.XmlPullParserException;
44import org.xmlpull.v1.XmlSerializer;
45
46import java.io.IOException;
47import java.io.PrintWriter;
48import java.util.ArrayList;
49import java.util.Collections;
50import java.util.List;
51import java.util.Map;
52import java.util.Map.Entry;
53
54public class RankingHelper implements RankingConfig {
55    private static final String TAG = "RankingHelper";
56
57    private static final int XML_VERSION = 1;
58
59    private static final String TAG_RANKING = "ranking";
60    private static final String TAG_PACKAGE = "package";
61    private static final String TAG_CHANNEL = "channel";
62
63    private static final String ATT_VERSION = "version";
64    private static final String ATT_NAME = "name";
65    private static final String ATT_UID = "uid";
66    private static final String ATT_ID = "id";
67    private static final String ATT_PRIORITY = "priority";
68    private static final String ATT_VISIBILITY = "visibility";
69    private static final String ATT_IMPORTANCE = "importance";
70
71    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
72    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
73    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
74
75    private final NotificationSignalExtractor[] mSignalExtractors;
76    private final NotificationComparator mPreliminaryComparator;
77    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
78
79    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
80    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
81    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
82
83    private final Context mContext;
84    private final RankingHandler mRankingHandler;
85    private final PackageManager mPm;
86
87    public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
88            NotificationUsageStats usageStats, String[] extractorNames) {
89        mContext = context;
90        mRankingHandler = rankingHandler;
91        mPm = pm;
92
93        mPreliminaryComparator = new NotificationComparator(mContext);
94
95        final int N = extractorNames.length;
96        mSignalExtractors = new NotificationSignalExtractor[N];
97        for (int i = 0; i < N; i++) {
98            try {
99                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
100                NotificationSignalExtractor extractor =
101                        (NotificationSignalExtractor) extractorClass.newInstance();
102                extractor.initialize(mContext, usageStats);
103                extractor.setConfig(this);
104                mSignalExtractors[i] = extractor;
105            } catch (ClassNotFoundException e) {
106                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
107            } catch (InstantiationException e) {
108                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
109            } catch (IllegalAccessException e) {
110                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
111            }
112        }
113    }
114
115    @SuppressWarnings("unchecked")
116    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
117        final int N = mSignalExtractors.length;
118        for (int i = 0; i < N; i++) {
119            final NotificationSignalExtractor extractor = mSignalExtractors[i];
120            if (extractorClass.equals(extractor.getClass())) {
121                return (T) extractor;
122            }
123        }
124        return null;
125    }
126
127    public void extractSignals(NotificationRecord r) {
128        final int N = mSignalExtractors.length;
129        for (int i = 0; i < N; i++) {
130            NotificationSignalExtractor extractor = mSignalExtractors[i];
131            try {
132                RankingReconsideration recon = extractor.process(r);
133                if (recon != null) {
134                    mRankingHandler.requestReconsideration(recon);
135                }
136            } catch (Throwable t) {
137                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
138            }
139        }
140    }
141
142    public void readXml(XmlPullParser parser, boolean forRestore)
143            throws XmlPullParserException, IOException {
144        int type = parser.getEventType();
145        if (type != XmlPullParser.START_TAG) return;
146        String tag = parser.getName();
147        if (!TAG_RANKING.equals(tag)) return;
148        mRecords.clear();
149        mRestoredWithoutUids.clear();
150        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
151            tag = parser.getName();
152            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
153                return;
154            }
155            if (type == XmlPullParser.START_TAG) {
156                if (TAG_PACKAGE.equals(tag)) {
157                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
158                    String name = parser.getAttributeValue(null, ATT_NAME);
159                    if (!TextUtils.isEmpty(name)) {
160                        if (forRestore) {
161                            try {
162                                //TODO: http://b/22388012
163                                uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
164                            } catch (NameNotFoundException e) {
165                                // noop
166                            }
167                        }
168
169                        Record r = getOrCreateRecord(name, uid,
170                                safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE),
171                                safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY),
172                                safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY));
173
174                        // Channels
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                                int channelImportance =
188                                        safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
189
190                                if (!TextUtils.isEmpty(id)) {
191                                    final NotificationChannel channel = new NotificationChannel(id,
192                                            channelName, channelImportance);
193                                    channel.populateFromXml(parser);
194                                    r.channels.put(id, channel);
195                                }
196                            }
197                        }
198
199                        clampDefaultChannel(r);
200                    }
201                }
202            }
203        }
204        throw new IllegalStateException("Failed to reach END_DOCUMENT");
205    }
206
207    private static String recordKey(String pkg, int uid) {
208        return pkg + "|" + uid;
209    }
210
211    private Record getRecord(String pkg, int uid) {
212        final String key = recordKey(pkg, uid);
213        return mRecords.get(key);
214    }
215
216    private Record getOrCreateRecord(String pkg, int uid) {
217        return getOrCreateRecord(pkg, uid,
218                DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY);
219    }
220
221    private Record getOrCreateRecord(String pkg, int uid, int importance, int priority,
222            int visibility) {
223        final String key = recordKey(pkg, uid);
224        Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get(key);
225        if (r == null) {
226            r = new Record();
227            r.pkg = pkg;
228            r.uid = uid;
229            r.importance = importance;
230            r.priority = priority;
231            r.visibility = visibility;
232            createDefaultChannelIfMissing(r);
233            if (r.uid == Record.UNKNOWN_UID) {
234                mRestoredWithoutUids.put(pkg, r);
235            } else {
236                mRecords.put(key, r);
237            }
238            clampDefaultChannel(r);
239        }
240        return r;
241    }
242
243    // Clamp the importance level of the default channel for apps targeting the new SDK version,
244    // unless the user has already changed the importance.
245    private void clampDefaultChannel(Record r) {
246        try {
247            if (r.uid != Record.UNKNOWN_UID) {
248                int userId = UserHandle.getUserId(r.uid);
249                final ApplicationInfo applicationInfo =
250                        mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
251                if (applicationInfo.targetSdkVersion > Build.VERSION_CODES.N_MR1) {
252                    final NotificationChannel defaultChannel =
253                            r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
254                    if ((defaultChannel.getUserLockedFields()
255                            & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
256                        defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW);
257                        updateConfig();
258                    }
259                }
260            }
261        } catch (NameNotFoundException e) {
262            // oh well.
263        }
264    }
265
266    private void createDefaultChannelIfMissing(Record r) {
267        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
268            NotificationChannel channel;
269            channel = new NotificationChannel(
270                    NotificationChannel.DEFAULT_CHANNEL_ID,
271                    mContext.getString(R.string.default_notification_channel_label),
272                    r.importance);
273            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
274            channel.setLockscreenVisibility(r.visibility);
275            if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
276                channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
277            }
278            if (r.priority != DEFAULT_PRIORITY) {
279                channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
280            }
281            if (r.visibility != DEFAULT_VISIBILITY) {
282                channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
283            }
284            r.channels.put(channel.getId(), channel);
285        }
286    }
287
288    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
289        out.startTag(null, TAG_RANKING);
290        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
291
292        final int N = mRecords.size();
293        for (int i = 0; i < N; i++) {
294            final Record r = mRecords.valueAt(i);
295            //TODO: http://b/22388012
296            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
297                continue;
298            }
299            final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
300                    || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
301                    || r.channels.size() > 0;
302            if (hasNonDefaultSettings) {
303                out.startTag(null, TAG_PACKAGE);
304                out.attribute(null, ATT_NAME, r.pkg);
305                if (r.importance != DEFAULT_IMPORTANCE) {
306                    out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
307                }
308                if (r.priority != DEFAULT_PRIORITY) {
309                    out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
310                }
311                if (r.visibility != DEFAULT_VISIBILITY) {
312                    out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
313                }
314
315                if (!forBackup) {
316                    out.attribute(null, ATT_UID, Integer.toString(r.uid));
317                }
318
319                for (NotificationChannel channel : r.channels.values()) {
320                    channel.writeXml(out);
321                }
322
323                out.endTag(null, TAG_PACKAGE);
324            }
325        }
326        out.endTag(null, TAG_RANKING);
327    }
328
329    private void updateConfig() {
330        final int N = mSignalExtractors.length;
331        for (int i = 0; i < N; i++) {
332            mSignalExtractors[i].setConfig(this);
333        }
334        mRankingHandler.requestSort(false);
335    }
336
337    public void sort(ArrayList<NotificationRecord> notificationList) {
338        final int N = notificationList.size();
339        // clear global sort keys
340        for (int i = N - 1; i >= 0; i--) {
341            notificationList.get(i).setGlobalSortKey(null);
342        }
343
344        // rank each record individually
345        Collections.sort(notificationList, mPreliminaryComparator);
346
347        synchronized (mProxyByGroupTmp) {
348            // record individual ranking result and nominate proxies for each group
349            for (int i = N - 1; i >= 0; i--) {
350                final NotificationRecord record = notificationList.get(i);
351                record.setAuthoritativeRank(i);
352                final String groupKey = record.getGroupKey();
353                boolean isGroupSummary = record.getNotification().isGroupSummary();
354                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
355                    mProxyByGroupTmp.put(groupKey, record);
356                }
357            }
358            // assign global sort key:
359            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
360            for (int i = 0; i < N; i++) {
361                final NotificationRecord record = notificationList.get(i);
362                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
363                String groupSortKey = record.getNotification().getSortKey();
364
365                // We need to make sure the developer provided group sort key (gsk) is handled
366                // correctly:
367                //   gsk="" < gsk=non-null-string < gsk=null
368                //
369                // We enforce this by using different prefixes for these three cases.
370                String groupSortKeyPortion;
371                if (groupSortKey == null) {
372                    groupSortKeyPortion = "nsk";
373                } else if (groupSortKey.equals("")) {
374                    groupSortKeyPortion = "esk";
375                } else {
376                    groupSortKeyPortion = "gsk=" + groupSortKey;
377                }
378
379                boolean isGroupSummary = record.getNotification().isGroupSummary();
380                record.setGlobalSortKey(
381                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
382                        record.isRecentlyIntrusive() ? '0' : '1',
383                        groupProxy.getAuthoritativeRank(),
384                        isGroupSummary ? '0' : '1',
385                        groupSortKeyPortion,
386                        record.getAuthoritativeRank()));
387            }
388            mProxyByGroupTmp.clear();
389        }
390
391        // Do a second ranking pass, using group proxies
392        Collections.sort(notificationList, mFinalComparator);
393    }
394
395    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
396        return Collections.binarySearch(notificationList, target, mFinalComparator);
397    }
398
399    private static int safeInt(XmlPullParser parser, String att, int defValue) {
400        final String val = parser.getAttributeValue(null, att);
401        return tryParseInt(val, defValue);
402    }
403
404    private static int tryParseInt(String value, int defValue) {
405        if (TextUtils.isEmpty(value)) return defValue;
406        try {
407            return Integer.parseInt(value);
408        } catch (NumberFormatException e) {
409            return defValue;
410        }
411    }
412
413    /**
414     * Gets importance.
415     */
416    @Override
417    public int getImportance(String packageName, int uid) {
418        return getOrCreateRecord(packageName, uid).importance;
419    }
420
421    @Override
422    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
423            boolean fromTargetApp) {
424        Preconditions.checkNotNull(pkg);
425        Preconditions.checkNotNull(channel);
426        Preconditions.checkNotNull(channel.getId());
427        Preconditions.checkNotNull(channel.getName());
428        Record r = getOrCreateRecord(pkg, uid);
429        if (r == null) {
430            throw new IllegalArgumentException("Invalid package");
431        }
432        if (IMPORTANCE_NONE == r.importance) {
433            throw new IllegalArgumentException("Package blocked");
434        }
435        NotificationChannel existing = r.channels.get(channel.getId());
436        // Keep existing settings
437        if (existing != null) {
438            if (existing.isDeleted()) {
439                existing.setDeleted(false);
440                updateConfig();
441            }
442            return;
443        }
444        if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
445                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
446            throw new IllegalArgumentException("Invalid importance level");
447        }
448        // Reset fields that apps aren't allowed to set.
449        if (fromTargetApp) {
450            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
451            channel.setLockscreenVisibility(r.visibility);
452        }
453        clearLockedFields(channel);
454        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
455            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
456        }
457        r.channels.put(channel.getId(), channel);
458        updateConfig();
459    }
460
461    private void clearLockedFields(NotificationChannel channel) {
462        int clearMask = 0;
463        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
464            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
465        }
466        channel.lockFields(~clearMask);
467    }
468
469    @Override
470    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
471        Preconditions.checkNotNull(updatedChannel);
472        Preconditions.checkNotNull(updatedChannel.getId());
473        Record r = getOrCreateRecord(pkg, uid);
474        if (r == null) {
475            throw new IllegalArgumentException("Invalid package");
476        }
477        NotificationChannel channel = r.channels.get(updatedChannel.getId());
478        if (channel == null || channel.isDeleted()) {
479            throw new IllegalArgumentException("Channel does not exist");
480        }
481        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
482            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
483        }
484        r.channels.put(updatedChannel.getId(), updatedChannel);
485        updateConfig();
486    }
487
488    @Override
489    public void updateNotificationChannelFromAssistant(String pkg, int uid,
490            NotificationChannel updatedChannel) {
491        Record r = getOrCreateRecord(pkg, uid);
492        if (r == null) {
493            throw new IllegalArgumentException("Invalid package");
494        }
495        NotificationChannel channel = r.channels.get(updatedChannel.getId());
496        if (channel == null || channel.isDeleted()) {
497            throw new IllegalArgumentException("Channel does not exist");
498        }
499
500        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
501            channel.setImportance(updatedChannel.getImportance());
502        }
503        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_LIGHTS) == 0) {
504            channel.setLights(updatedChannel.shouldShowLights());
505        }
506        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0) {
507            channel.setBypassDnd(updatedChannel.canBypassDnd());
508        }
509        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) {
510            channel.setSound(updatedChannel.getSound());
511        }
512        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
513            channel.enableVibration(updatedChannel.shouldVibrate());
514            channel.setVibrationPattern(updatedChannel.getVibrationPattern());
515        }
516        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) {
517            if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
518                channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
519            } else {
520                channel.setLockscreenVisibility(updatedChannel.getLockscreenVisibility());
521            }
522        }
523        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0) {
524            channel.setShowBadge(updatedChannel.canShowBadge());
525        }
526        if (updatedChannel.isDeleted()) {
527            updatedChannel.setDeleted(true);
528        }
529
530        r.channels.put(channel.getId(), channel);
531        updateConfig();
532    }
533
534    @Override
535    public NotificationChannel getNotificationChannelWithFallback(String pkg, int uid,
536            String channelId, boolean includeDeleted) {
537        Record r = getOrCreateRecord(pkg, uid);
538        if (channelId == null) {
539            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
540        }
541        NotificationChannel channel = r.channels.get(channelId);
542        if (channel != null && (includeDeleted || !channel.isDeleted())) {
543            return channel;
544        } else {
545            return r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
546        }
547    }
548
549    @Override
550    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
551            boolean includeDeleted) {
552        Preconditions.checkNotNull(pkg);
553        Record r = getOrCreateRecord(pkg, uid);
554        if (r == null) {
555            return null;
556        }
557        if (channelId == null) {
558            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
559        }
560        final NotificationChannel nc = r.channels.get(channelId);
561        if (nc != null && (includeDeleted || !nc.isDeleted())) {
562            return nc;
563        }
564        return null;
565    }
566
567    @Override
568    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
569        Preconditions.checkNotNull(pkg);
570        Preconditions.checkNotNull(channelId);
571        Record r = getRecord(pkg, uid);
572        if (r == null) {
573            return;
574        }
575        NotificationChannel channel = r.channels.get(channelId);
576        if (channel != null) {
577            channel.setDeleted(true);
578        }
579    }
580
581    @Override
582    @VisibleForTesting
583    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
584        Preconditions.checkNotNull(pkg);
585        Preconditions.checkNotNull(channelId);
586        Record r = getRecord(pkg, uid);
587        if (r == null) {
588            return;
589        }
590        r.channels.remove(channelId);
591    }
592
593    @Override
594    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
595        Preconditions.checkNotNull(pkg);
596        Record r = getRecord(pkg, uid);
597        if (r == null) {
598            return;
599        }
600        int N = r.channels.size() - 1;
601        for (int i = N; i >= 0; i--) {
602            String key = r.channels.keyAt(i);
603            if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) {
604                r.channels.remove(key);
605            }
606        }
607    }
608
609    @Override
610    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
611            boolean includeDeleted) {
612        Preconditions.checkNotNull(pkg);
613        List<NotificationChannel> channels = new ArrayList<>();
614        Record r = getRecord(pkg, uid);
615        if (r == null) {
616            return ParceledListSlice.emptyList();
617        }
618        int N = r.channels.size();
619        for (int i = 0; i < N; i++) {
620            final NotificationChannel nc = r.channels.valueAt(i);
621            if (includeDeleted || !nc.isDeleted()) {
622                channels.add(nc);
623            }
624        }
625        return new ParceledListSlice<>(channels);
626    }
627
628    /**
629     * Sets importance.
630     */
631    @Override
632    public void setImportance(String pkgName, int uid, int importance) {
633        getOrCreateRecord(pkgName, uid).importance = importance;
634        updateConfig();
635    }
636
637    public void setEnabled(String packageName, int uid, boolean enabled) {
638        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
639        if (wasEnabled == enabled) {
640            return;
641        }
642        setImportance(packageName, uid,
643                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
644    }
645
646    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
647        if (filter == null) {
648            final int N = mSignalExtractors.length;
649            pw.print(prefix);
650            pw.print("mSignalExtractors.length = ");
651            pw.println(N);
652            for (int i = 0; i < N; i++) {
653                pw.print(prefix);
654                pw.print("  ");
655                pw.println(mSignalExtractors[i]);
656            }
657        }
658        if (filter == null) {
659            pw.print(prefix);
660            pw.println("per-package config:");
661        }
662        pw.println("Records:");
663        dumpRecords(pw, prefix, filter, mRecords);
664        pw.println("Restored without uid:");
665        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
666    }
667
668    private static void dumpRecords(PrintWriter pw, String prefix,
669            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
670        final int N = records.size();
671        for (int i = 0; i < N; i++) {
672            final Record r = records.valueAt(i);
673            if (filter == null || filter.matches(r.pkg)) {
674                pw.print(prefix);
675                pw.print("  ");
676                pw.print(r.pkg);
677                pw.print(" (");
678                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
679                pw.print(')');
680                if (r.importance != DEFAULT_IMPORTANCE) {
681                    pw.print(" importance=");
682                    pw.print(Ranking.importanceToString(r.importance));
683                }
684                if (r.priority != DEFAULT_PRIORITY) {
685                    pw.print(" priority=");
686                    pw.print(Notification.priorityToString(r.priority));
687                }
688                if (r.visibility != DEFAULT_VISIBILITY) {
689                    pw.print(" visibility=");
690                    pw.print(Notification.visibilityToString(r.visibility));
691                }
692                pw.println();
693                for (NotificationChannel channel : r.channels.values()) {
694                    pw.print(prefix);
695                    pw.print("  ");
696                    pw.print("  ");
697                    pw.println(channel);
698                }
699            }
700        }
701    }
702
703    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
704        JSONObject ranking = new JSONObject();
705        JSONArray records = new JSONArray();
706        try {
707            ranking.put("noUid", mRestoredWithoutUids.size());
708        } catch (JSONException e) {
709           // pass
710        }
711        final int N = mRecords.size();
712        for (int i = 0; i < N; i++) {
713            final Record r = mRecords.valueAt(i);
714            if (filter == null || filter.matches(r.pkg)) {
715                JSONObject record = new JSONObject();
716                try {
717                    record.put("userId", UserHandle.getUserId(r.uid));
718                    record.put("packageName", r.pkg);
719                    if (r.importance != DEFAULT_IMPORTANCE) {
720                        record.put("importance", Ranking.importanceToString(r.importance));
721                    }
722                    if (r.priority != DEFAULT_PRIORITY) {
723                        record.put("priority", Notification.priorityToString(r.priority));
724                    }
725                    if (r.visibility != DEFAULT_VISIBILITY) {
726                        record.put("visibility", Notification.visibilityToString(r.visibility));
727                    }
728                    for (NotificationChannel channel : r.channels.values()) {
729                        record.put("channel", channel.toJson());
730                    }
731                } catch (JSONException e) {
732                   // pass
733                }
734                records.put(record);
735            }
736        }
737        try {
738            ranking.put("records", records);
739        } catch (JSONException e) {
740            // pass
741        }
742        return ranking;
743    }
744
745    /**
746     * Dump only the ban information as structured JSON for the stats collector.
747     *
748     * This is intentionally redundant with {#link dumpJson} because the old
749     * scraper will expect this format.
750     *
751     * @param filter
752     * @return
753     */
754    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
755        JSONArray bans = new JSONArray();
756        Map<Integer, String> packageBans = getPackageBans();
757        for(Entry<Integer, String> ban : packageBans.entrySet()) {
758            final int userId = UserHandle.getUserId(ban.getKey());
759            final String packageName = ban.getValue();
760            if (filter == null || filter.matches(packageName)) {
761                JSONObject banJson = new JSONObject();
762                try {
763                    banJson.put("userId", userId);
764                    banJson.put("packageName", packageName);
765                } catch (JSONException e) {
766                    e.printStackTrace();
767                }
768                bans.put(banJson);
769            }
770        }
771        return bans;
772    }
773
774    public Map<Integer, String> getPackageBans() {
775        final int N = mRecords.size();
776        ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
777        for (int i = 0; i < N; i++) {
778            final Record r = mRecords.valueAt(i);
779            if (r.importance == NotificationManager.IMPORTANCE_NONE) {
780                packageBans.put(r.uid, r.pkg);
781            }
782        }
783        return packageBans;
784    }
785
786    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
787            int[] uidList) {
788        if (pkgList == null || pkgList.length == 0) {
789            return; // nothing to do
790        }
791        boolean updated = false;
792        if (removingPackage) {
793            // Remove notification settings for uninstalled package
794            int size = Math.min(pkgList.length, uidList.length);
795            for (int i = 0; i < size; i++) {
796                final String pkg = pkgList[i];
797                final int uid = uidList[i];
798                mRecords.remove(recordKey(pkg, uid));
799                mRestoredWithoutUids.remove(pkg);
800                updated = true;
801            }
802        } else {
803            for (String pkg : pkgList) {
804                // Package install
805                final Record r = mRestoredWithoutUids.get(pkg);
806                if (r != null) {
807                    try {
808                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
809                        mRestoredWithoutUids.remove(pkg);
810                        mRecords.put(recordKey(r.pkg, r.uid), r);
811                        updated = true;
812                    } catch (NameNotFoundException e) {
813                        // noop
814                    }
815                }
816                // Package upgrade
817                try {
818                    Record fullRecord = getRecord(pkg,
819                            mPm.getPackageUidAsUser(pkg, changeUserId));
820                    if (fullRecord != null) {
821                        clampDefaultChannel(fullRecord);
822                    }
823                } catch (NameNotFoundException e) {
824                }
825            }
826        }
827
828        if (updated) {
829            updateConfig();
830        }
831    }
832
833    private static class Record {
834        static int UNKNOWN_UID = UserHandle.USER_NULL;
835
836        String pkg;
837        int uid = UNKNOWN_UID;
838        int importance = DEFAULT_IMPORTANCE;
839        int priority = DEFAULT_PRIORITY;
840        int visibility = DEFAULT_VISIBILITY;
841
842        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
843   }
844}
845