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