RankingHelper.java revision 440119679893ee3d4fcccad8ce9faaaf8e54dcad
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;
19import com.android.internal.annotations.VisibleForTesting;
20import com.android.internal.logging.MetricsLogger;
21import com.android.internal.logging.nano.MetricsProto;
22import com.android.internal.util.Preconditions;
23
24import android.app.Notification;
25import android.app.NotificationChannel;
26import android.app.NotificationChannelGroup;
27import android.app.NotificationManager;
28import android.content.Context;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManager.NameNotFoundException;
32import android.content.pm.ParceledListSlice;
33import android.metrics.LogMaker;
34import android.os.Build;
35import android.os.UserHandle;
36import android.provider.Settings;
37import android.service.notification.NotificationListenerService.Ranking;
38import android.text.TextUtils;
39import android.util.ArrayMap;
40import android.util.Slog;
41
42import org.json.JSONArray;
43import org.json.JSONException;
44import org.json.JSONObject;
45import org.xmlpull.v1.XmlPullParser;
46import org.xmlpull.v1.XmlPullParserException;
47import org.xmlpull.v1.XmlSerializer;
48
49import java.io.IOException;
50import java.io.PrintWriter;
51import java.util.ArrayList;
52import java.util.Collection;
53import java.util.Collections;
54import java.util.List;
55import java.util.Map;
56import java.util.Map.Entry;
57
58public class RankingHelper implements RankingConfig {
59    private static final String TAG = "RankingHelper";
60
61    private static final int XML_VERSION = 1;
62
63    private static final String TAG_RANKING = "ranking";
64    private static final String TAG_PACKAGE = "package";
65    private static final String TAG_CHANNEL = "channel";
66    private static final String TAG_GROUP = "channelGroup";
67
68    private static final String ATT_VERSION = "version";
69    private static final String ATT_NAME = "name";
70    private static final String ATT_UID = "uid";
71    private static final String ATT_ID = "id";
72    private static final String ATT_PRIORITY = "priority";
73    private static final String ATT_VISIBILITY = "visibility";
74    private static final String ATT_IMPORTANCE = "importance";
75    private static final String ATT_SHOW_BADGE = "show_badge";
76
77    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
78    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
79    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
80    private static final boolean DEFAULT_SHOW_BADGE = true;
81
82    private final NotificationSignalExtractor[] mSignalExtractors;
83    private final NotificationComparator mPreliminaryComparator;
84    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
85
86    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
87    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
88    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
89
90    private final Context mContext;
91    private final RankingHandler mRankingHandler;
92    private final PackageManager mPm;
93
94    public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
95            NotificationUsageStats usageStats, String[] extractorNames) {
96        mContext = context;
97        mRankingHandler = rankingHandler;
98        mPm = pm;
99
100        mPreliminaryComparator = new NotificationComparator(mContext);
101
102        final int N = extractorNames.length;
103        mSignalExtractors = new NotificationSignalExtractor[N];
104        for (int i = 0; i < N; i++) {
105            try {
106                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
107                NotificationSignalExtractor extractor =
108                        (NotificationSignalExtractor) extractorClass.newInstance();
109                extractor.initialize(mContext, usageStats);
110                extractor.setConfig(this);
111                mSignalExtractors[i] = extractor;
112            } catch (ClassNotFoundException e) {
113                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
114            } catch (InstantiationException e) {
115                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
116            } catch (IllegalAccessException e) {
117                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
118            }
119        }
120    }
121
122    @SuppressWarnings("unchecked")
123    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
124        final int N = mSignalExtractors.length;
125        for (int i = 0; i < N; i++) {
126            final NotificationSignalExtractor extractor = mSignalExtractors[i];
127            if (extractorClass.equals(extractor.getClass())) {
128                return (T) extractor;
129            }
130        }
131        return null;
132    }
133
134    public void extractSignals(NotificationRecord r) {
135        final int N = mSignalExtractors.length;
136        for (int i = 0; i < N; i++) {
137            NotificationSignalExtractor extractor = mSignalExtractors[i];
138            try {
139                RankingReconsideration recon = extractor.process(r);
140                if (recon != null) {
141                    mRankingHandler.requestReconsideration(recon);
142                }
143            } catch (Throwable t) {
144                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
145            }
146        }
147    }
148
149    public void readXml(XmlPullParser parser, boolean forRestore)
150            throws XmlPullParserException, IOException {
151        int type = parser.getEventType();
152        if (type != XmlPullParser.START_TAG) return;
153        String tag = parser.getName();
154        if (!TAG_RANKING.equals(tag)) return;
155        // Clobber groups and channels with the xml, but don't delete other data that wasn't present
156        // at the time of serialization.
157        mRestoredWithoutUids.clear();
158        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
159            tag = parser.getName();
160            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
161                return;
162            }
163            if (type == XmlPullParser.START_TAG) {
164                if (TAG_PACKAGE.equals(tag)) {
165                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
166                    String name = parser.getAttributeValue(null, ATT_NAME);
167                    if (!TextUtils.isEmpty(name)) {
168                        if (forRestore) {
169                            try {
170                                //TODO: http://b/22388012
171                                uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
172                            } catch (NameNotFoundException e) {
173                                // noop
174                            }
175                        }
176
177                        Record r = getOrCreateRecord(name, uid,
178                                safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE),
179                                safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY),
180                                safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY),
181                                safeBool(parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE));
182
183                        final int innerDepth = parser.getDepth();
184                        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
185                                && (type != XmlPullParser.END_TAG
186                                || parser.getDepth() > innerDepth)) {
187                            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
188                                continue;
189                            }
190
191                            String tagName = parser.getName();
192                            // Channel groups
193                            if (TAG_GROUP.equals(tagName)) {
194                                String id = parser.getAttributeValue(null, ATT_ID);
195                                CharSequence groupName = parser.getAttributeValue(null, ATT_NAME);
196                                if (!TextUtils.isEmpty(id)) {
197                                    NotificationChannelGroup group
198                                            = new NotificationChannelGroup(id, groupName);
199                                    r.groups.put(id, group);
200                                }
201                            }
202                            // Channels
203                            if (TAG_CHANNEL.equals(tagName)) {
204                                String id = parser.getAttributeValue(null, ATT_ID);
205                                String channelName = parser.getAttributeValue(null, ATT_NAME);
206                                int channelImportance =
207                                        safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
208                                if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
209                                    NotificationChannel channel = new NotificationChannel(id,
210                                            channelName, channelImportance);
211                                    channel.populateFromXml(parser);
212                                    r.channels.put(id, channel);
213                                }
214                            }
215                        }
216
217                        try {
218                            deleteDefaultChannelIfNeeded(r);
219                        } catch (NameNotFoundException e) {
220                            Slog.e(TAG, "deleteDefaultChannelIfNeeded - Exception: " + e);
221                        }
222                    }
223                }
224            }
225        }
226        throw new IllegalStateException("Failed to reach END_DOCUMENT");
227    }
228
229    private static String recordKey(String pkg, int uid) {
230        return pkg + "|" + uid;
231    }
232
233    private Record getRecord(String pkg, int uid) {
234        final String key = recordKey(pkg, uid);
235        synchronized (mRecords) {
236            return mRecords.get(key);
237        }
238    }
239
240    private Record getOrCreateRecord(String pkg, int uid) {
241        return getOrCreateRecord(pkg, uid,
242                DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE);
243    }
244
245    private Record getOrCreateRecord(String pkg, int uid, int importance, int priority,
246            int visibility, boolean showBadge) {
247        final String key = recordKey(pkg, uid);
248        synchronized (mRecords) {
249            Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get(
250                    key);
251            if (r == null) {
252                r = new Record();
253                r.pkg = pkg;
254                r.uid = uid;
255                r.importance = importance;
256                r.priority = priority;
257                r.visibility = visibility;
258                r.showBadge = showBadge;
259
260                try {
261                    createDefaultChannelIfNeeded(r);
262                } catch (NameNotFoundException e) {
263                    Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e);
264                }
265
266                if (r.uid == Record.UNKNOWN_UID) {
267                    mRestoredWithoutUids.put(pkg, r);
268                } else {
269                    mRecords.put(key, r);
270                }
271            }
272            return r;
273        }
274    }
275
276    private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException {
277        final int userId = UserHandle.getUserId(r.uid);
278        final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
279        if (applicationInfo.targetSdkVersion <= Build.VERSION_CODES.N_MR1) {
280            // Pre-O apps should have it.
281            return true;
282        }
283
284        // STOPSHIP TODO: remove before release - O+ apps should never have a default channel.
285        // But for now, leave the default channel until an app has created its first channel.
286        boolean hasCreatedAChannel = false;
287        final int size = r.channels.size();
288        for (int i = 0; i < size; i++) {
289            final NotificationChannel notificationChannel = r.channels.valueAt(i);
290            if (notificationChannel != null &&
291                    !notificationChannel.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
292                hasCreatedAChannel = true;
293                break;
294            }
295        }
296        if (!hasCreatedAChannel) {
297            return true;
298        }
299
300        // Otherwise, should not have the default channel.
301        return false;
302    }
303
304    private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
305        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
306            // Not present
307            return;
308        }
309
310        if (shouldHaveDefaultChannel(r)) {
311            // Keep the default channel until upgraded.
312            return;
313        }
314
315        // Remove Default Channel.
316        r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID);
317    }
318
319    private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
320        if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
321            // Already exists
322            return;
323        }
324
325        if (!shouldHaveDefaultChannel(r)) {
326            // Keep the default channel until upgraded.
327            return;
328        }
329
330        // Create Default Channel
331        NotificationChannel channel;
332        channel = new NotificationChannel(
333                NotificationChannel.DEFAULT_CHANNEL_ID,
334                mContext.getString(R.string.default_notification_channel_label),
335                r.importance);
336        channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
337        channel.setLockscreenVisibility(r.visibility);
338        if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
339            channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
340        }
341        if (r.priority != DEFAULT_PRIORITY) {
342            channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
343        }
344        if (r.visibility != DEFAULT_VISIBILITY) {
345            channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
346        }
347        r.channels.put(channel.getId(), channel);
348    }
349
350    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
351        out.startTag(null, TAG_RANKING);
352        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
353
354        synchronized (mRecords) {
355            final int N = mRecords.size();
356            for (int i = 0; i < N; i++) {
357                final Record r = mRecords.valueAt(i);
358                //TODO: http://b/22388012
359                if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
360                    continue;
361                }
362                final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
363                        || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
364                        || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
365                        || r.groups.size() > 0;
366                if (hasNonDefaultSettings) {
367                    out.startTag(null, TAG_PACKAGE);
368                    out.attribute(null, ATT_NAME, r.pkg);
369                    if (r.importance != DEFAULT_IMPORTANCE) {
370                        out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
371                    }
372                    if (r.priority != DEFAULT_PRIORITY) {
373                        out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
374                    }
375                    if (r.visibility != DEFAULT_VISIBILITY) {
376                        out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
377                    }
378                    out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
379
380                    if (!forBackup) {
381                        out.attribute(null, ATT_UID, Integer.toString(r.uid));
382                    }
383
384                    for (NotificationChannelGroup group : r.groups.values()) {
385                        group.writeXml(out);
386                    }
387
388                    for (NotificationChannel channel : r.channels.values()) {
389                        if (!forBackup || (forBackup && !channel.isDeleted())) {
390                            channel.writeXml(out);
391                        }
392                    }
393
394                    out.endTag(null, TAG_PACKAGE);
395                }
396            }
397        }
398        out.endTag(null, TAG_RANKING);
399    }
400
401    private void updateConfig() {
402        final int N = mSignalExtractors.length;
403        for (int i = 0; i < N; i++) {
404            mSignalExtractors[i].setConfig(this);
405        }
406        mRankingHandler.requestSort(false);
407    }
408
409    public void sort(ArrayList<NotificationRecord> notificationList) {
410        final int N = notificationList.size();
411        // clear global sort keys
412        for (int i = N - 1; i >= 0; i--) {
413            notificationList.get(i).setGlobalSortKey(null);
414        }
415
416        // rank each record individually
417        Collections.sort(notificationList, mPreliminaryComparator);
418
419        synchronized (mProxyByGroupTmp) {
420            // record individual ranking result and nominate proxies for each group
421            for (int i = N - 1; i >= 0; i--) {
422                final NotificationRecord record = notificationList.get(i);
423                record.setAuthoritativeRank(i);
424                final String groupKey = record.getGroupKey();
425                NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
426                if (existingProxy == null
427                        || record.getImportance() > existingProxy.getImportance()) {
428                    mProxyByGroupTmp.put(groupKey, record);
429                }
430            }
431            // assign global sort key:
432            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
433            for (int i = 0; i < N; i++) {
434                final NotificationRecord record = notificationList.get(i);
435                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
436                String groupSortKey = record.getNotification().getSortKey();
437
438                // We need to make sure the developer provided group sort key (gsk) is handled
439                // correctly:
440                //   gsk="" < gsk=non-null-string < gsk=null
441                //
442                // We enforce this by using different prefixes for these three cases.
443                String groupSortKeyPortion;
444                if (groupSortKey == null) {
445                    groupSortKeyPortion = "nsk";
446                } else if (groupSortKey.equals("")) {
447                    groupSortKeyPortion = "esk";
448                } else {
449                    groupSortKeyPortion = "gsk=" + groupSortKey;
450                }
451
452                boolean isGroupSummary = record.getNotification().isGroupSummary();
453                record.setGlobalSortKey(
454                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
455                        record.isRecentlyIntrusive() ? '0' : '1',
456                        groupProxy.getAuthoritativeRank(),
457                        isGroupSummary ? '0' : '1',
458                        groupSortKeyPortion,
459                        record.getAuthoritativeRank()));
460            }
461            mProxyByGroupTmp.clear();
462        }
463
464        // Do a second ranking pass, using group proxies
465        Collections.sort(notificationList, mFinalComparator);
466    }
467
468    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
469        return Collections.binarySearch(notificationList, target, mFinalComparator);
470    }
471
472    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
473        final String value = parser.getAttributeValue(null, att);
474        if (TextUtils.isEmpty(value)) return defValue;
475        return Boolean.parseBoolean(value);
476    }
477
478    private static int safeInt(XmlPullParser parser, String att, int defValue) {
479        final String val = parser.getAttributeValue(null, att);
480        return tryParseInt(val, defValue);
481    }
482
483    private static int tryParseInt(String value, int defValue) {
484        if (TextUtils.isEmpty(value)) return defValue;
485        try {
486            return Integer.parseInt(value);
487        } catch (NumberFormatException e) {
488            return defValue;
489        }
490    }
491
492    /**
493     * Gets importance.
494     */
495    @Override
496    public int getImportance(String packageName, int uid) {
497        return getOrCreateRecord(packageName, uid).importance;
498    }
499
500    @Override
501    public boolean canShowBadge(String packageName, int uid) {
502        return getOrCreateRecord(packageName, uid).showBadge;
503    }
504
505    @Override
506    public void setShowBadge(String packageName, int uid, boolean showBadge) {
507        getOrCreateRecord(packageName, uid).showBadge = showBadge;
508        updateConfig();
509    }
510
511    @Override
512    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
513            boolean fromTargetApp) {
514        Preconditions.checkNotNull(pkg);
515        Preconditions.checkNotNull(group);
516        Preconditions.checkNotNull(group.getId());
517        Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName()));
518        Record r = getOrCreateRecord(pkg, uid);
519        if (r == null) {
520            throw new IllegalArgumentException("Invalid package");
521        }
522        LogMaker lm = new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP)
523                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
524                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID,
525                        group.getId())
526                .setPackageName(pkg);
527        MetricsLogger.action(lm);
528        r.groups.put(group.getId(), group);
529        updateConfig();
530    }
531
532    @Override
533    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
534            boolean fromTargetApp) {
535        Preconditions.checkNotNull(pkg);
536        Preconditions.checkNotNull(channel);
537        Preconditions.checkNotNull(channel.getId());
538        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
539        Record r = getOrCreateRecord(pkg, uid);
540        if (r == null) {
541            throw new IllegalArgumentException("Invalid package");
542        }
543        if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
544            throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
545        }
546        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
547            throw new IllegalArgumentException("Reserved id");
548        }
549
550        NotificationChannel existing = r.channels.get(channel.getId());
551        // Keep existing settings, except deleted status and name
552        if (existing != null && fromTargetApp) {
553            if (existing.isDeleted()) {
554                existing.setDeleted(false);
555            }
556
557            existing.setName(channel.getName().toString());
558            existing.setDescription(channel.getDescription());
559
560            MetricsLogger.action(getChannelLog(channel, pkg));
561            updateConfig();
562            return;
563        }
564        if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
565                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
566            throw new IllegalArgumentException("Invalid importance level");
567        }
568        // Reset fields that apps aren't allowed to set.
569        if (fromTargetApp) {
570            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
571            channel.setLockscreenVisibility(r.visibility);
572        }
573        clearLockedFields(channel);
574        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
575            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
576        }
577        if (!r.showBadge) {
578            channel.setShowBadge(false);
579        }
580        r.channels.put(channel.getId(), channel);
581        MetricsLogger.action(getChannelLog(channel, pkg).setType(
582                MetricsProto.MetricsEvent.TYPE_OPEN));
583        updateConfig();
584    }
585
586    private void clearLockedFields(NotificationChannel channel) {
587        int clearMask = 0;
588        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
589            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
590        }
591        channel.lockFields(~clearMask);
592    }
593
594    @Override
595    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
596        Preconditions.checkNotNull(updatedChannel);
597        Preconditions.checkNotNull(updatedChannel.getId());
598        Record r = getOrCreateRecord(pkg, uid);
599        if (r == null) {
600            throw new IllegalArgumentException("Invalid package");
601        }
602        NotificationChannel channel = r.channels.get(updatedChannel.getId());
603        if (channel == null || channel.isDeleted()) {
604            throw new IllegalArgumentException("Channel does not exist");
605        }
606        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
607            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
608        }
609        r.channels.put(updatedChannel.getId(), updatedChannel);
610
611        MetricsLogger.action(getChannelLog(updatedChannel, pkg));
612        updateConfig();
613    }
614
615    @Override
616    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
617            boolean includeDeleted) {
618        Preconditions.checkNotNull(pkg);
619        Record r = getOrCreateRecord(pkg, uid);
620        if (r == null) {
621            return null;
622        }
623        if (channelId == null) {
624            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
625        }
626        final NotificationChannel nc = r.channels.get(channelId);
627        if (nc != null && (includeDeleted || !nc.isDeleted())) {
628            return nc;
629        }
630        return null;
631    }
632
633    @Override
634    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
635        Record r = getRecord(pkg, uid);
636        if (r == null) {
637            return;
638        }
639        NotificationChannel channel = r.channels.get(channelId);
640        if (channel != null) {
641            channel.setDeleted(true);
642            LogMaker lm = getChannelLog(channel, pkg);
643            lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE);
644            MetricsLogger.action(lm);
645            updateConfig();
646        }
647    }
648
649    @Override
650    @VisibleForTesting
651    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
652        Preconditions.checkNotNull(pkg);
653        Preconditions.checkNotNull(channelId);
654        Record r = getRecord(pkg, uid);
655        if (r == null) {
656            return;
657        }
658        r.channels.remove(channelId);
659        updateConfig();
660    }
661
662    @Override
663    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
664        Preconditions.checkNotNull(pkg);
665        Record r = getRecord(pkg, uid);
666        if (r == null) {
667            return;
668        }
669        int N = r.channels.size() - 1;
670        for (int i = N; i >= 0; i--) {
671            String key = r.channels.keyAt(i);
672            if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) {
673                r.channels.remove(key);
674            }
675        }
676        updateConfig();
677    }
678
679    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
680            int uid) {
681        Preconditions.checkNotNull(pkg);
682        Record r = getRecord(pkg, uid);
683        return r.groups.get(groupId);
684    }
685
686    @Override
687    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
688            int uid, boolean includeDeleted) {
689        Preconditions.checkNotNull(pkg);
690        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
691        Record r = getRecord(pkg, uid);
692        if (r == null) {
693            return ParceledListSlice.emptyList();
694        }
695        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
696        int N = r.channels.size();
697        for (int i = 0; i < N; i++) {
698            final NotificationChannel nc = r.channels.valueAt(i);
699            if (includeDeleted || !nc.isDeleted()) {
700                if (nc.getGroup() != null) {
701                    if (r.groups.get(nc.getGroup()) != null) {
702                        NotificationChannelGroup ncg = groups.get(nc.getGroup());
703                        if (ncg == null) {
704                            ncg = r.groups.get(nc.getGroup()).clone();
705                            groups.put(nc.getGroup(), ncg);
706
707                        }
708                        ncg.addChannel(nc);
709                    }
710                } else {
711                    nonGrouped.addChannel(nc);
712                }
713            }
714        }
715        if (nonGrouped.getChannels().size() > 0) {
716            groups.put(null, nonGrouped);
717        }
718        return new ParceledListSlice<>(new ArrayList<>(groups.values()));
719    }
720
721    public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid,
722            String groupId) {
723        List<NotificationChannel> deletedChannels = new ArrayList<>();
724        Record r = getRecord(pkg, uid);
725        if (r == null || TextUtils.isEmpty(groupId)) {
726            return deletedChannels;
727        }
728
729        r.groups.remove(groupId);
730
731        int N = r.channels.size();
732        for (int i = 0; i < N; i++) {
733            final NotificationChannel nc = r.channels.valueAt(i);
734            if (groupId.equals(nc.getGroup())) {
735                nc.setDeleted(true);
736                deletedChannels.add(nc);
737            }
738        }
739        updateConfig();
740        return deletedChannels;
741    }
742
743    @Override
744    public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
745            int uid) {
746        Record r = getRecord(pkg, uid);
747        if (r == null) {
748            return new ArrayList<>();
749        }
750        return r.groups.values();
751    }
752
753    @Override
754    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
755            boolean includeDeleted) {
756        Preconditions.checkNotNull(pkg);
757        List<NotificationChannel> channels = new ArrayList<>();
758        Record r = getRecord(pkg, uid);
759        if (r == null) {
760            return ParceledListSlice.emptyList();
761        }
762        int N = r.channels.size();
763        for (int i = 0; i < N; i++) {
764            final NotificationChannel nc = r.channels.valueAt(i);
765            if (includeDeleted || !nc.isDeleted()) {
766                channels.add(nc);
767            }
768        }
769        return new ParceledListSlice<>(channels);
770    }
771
772    public int getDeletedChannelCount(String pkg, int uid) {
773        Preconditions.checkNotNull(pkg);
774        int deletedCount = 0;
775        Record r = getRecord(pkg, uid);
776        if (r == null) {
777            return deletedCount;
778        }
779        int N = r.channels.size();
780        for (int i = 0; i < N; i++) {
781            final NotificationChannel nc = r.channels.valueAt(i);
782            if (nc.isDeleted()) {
783                deletedCount++;
784            }
785        }
786        return deletedCount;
787    }
788
789    /**
790     * Sets importance.
791     */
792    @Override
793    public void setImportance(String pkgName, int uid, int importance) {
794        getOrCreateRecord(pkgName, uid).importance = importance;
795        updateConfig();
796    }
797
798    public void setEnabled(String packageName, int uid, boolean enabled) {
799        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
800        if (wasEnabled == enabled) {
801            return;
802        }
803        setImportance(packageName, uid,
804                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
805    }
806
807    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
808        if (filter == null) {
809            final int N = mSignalExtractors.length;
810            pw.print(prefix);
811            pw.print("mSignalExtractors.length = ");
812            pw.println(N);
813            for (int i = 0; i < N; i++) {
814                pw.print(prefix);
815                pw.print("  ");
816                pw.println(mSignalExtractors[i]);
817            }
818        }
819        if (filter == null) {
820            pw.print(prefix);
821            pw.println("per-package config:");
822        }
823        pw.println("Records:");
824        synchronized (mRecords) {
825            dumpRecords(pw, prefix, filter, mRecords);
826        }
827        pw.println("Restored without uid:");
828        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
829    }
830
831    private static void dumpRecords(PrintWriter pw, String prefix,
832            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
833        final int N = records.size();
834        for (int i = 0; i < N; i++) {
835            final Record r = records.valueAt(i);
836            if (filter == null || filter.matches(r.pkg)) {
837                pw.print(prefix);
838                pw.print("  AppSettings: ");
839                pw.print(r.pkg);
840                pw.print(" (");
841                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
842                pw.print(')');
843                if (r.importance != DEFAULT_IMPORTANCE) {
844                    pw.print(" importance=");
845                    pw.print(Ranking.importanceToString(r.importance));
846                }
847                if (r.priority != DEFAULT_PRIORITY) {
848                    pw.print(" priority=");
849                    pw.print(Notification.priorityToString(r.priority));
850                }
851                if (r.visibility != DEFAULT_VISIBILITY) {
852                    pw.print(" visibility=");
853                    pw.print(Notification.visibilityToString(r.visibility));
854                }
855                pw.print(" showBadge=");
856                pw.print(Boolean.toString(r.showBadge));
857                pw.println();
858                for (NotificationChannel channel : r.channels.values()) {
859                    pw.print(prefix);
860                    pw.print("  ");
861                    pw.print("  ");
862                    pw.println(channel);
863                }
864                for (NotificationChannelGroup group : r.groups.values()) {
865                    pw.print(prefix);
866                    pw.print("  ");
867                    pw.print("  ");
868                    pw.println(group);
869                }
870            }
871        }
872    }
873
874    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
875        JSONObject ranking = new JSONObject();
876        JSONArray records = new JSONArray();
877        try {
878            ranking.put("noUid", mRestoredWithoutUids.size());
879        } catch (JSONException e) {
880           // pass
881        }
882        synchronized (mRecords) {
883            final int N = mRecords.size();
884            for (int i = 0; i < N; i++) {
885                final Record r = mRecords.valueAt(i);
886                if (filter == null || filter.matches(r.pkg)) {
887                    JSONObject record = new JSONObject();
888                    try {
889                        record.put("userId", UserHandle.getUserId(r.uid));
890                        record.put("packageName", r.pkg);
891                        if (r.importance != DEFAULT_IMPORTANCE) {
892                            record.put("importance", Ranking.importanceToString(r.importance));
893                        }
894                        if (r.priority != DEFAULT_PRIORITY) {
895                            record.put("priority", Notification.priorityToString(r.priority));
896                        }
897                        if (r.visibility != DEFAULT_VISIBILITY) {
898                            record.put("visibility", Notification.visibilityToString(r.visibility));
899                        }
900                        if (r.showBadge != DEFAULT_SHOW_BADGE) {
901                            record.put("showBadge", Boolean.valueOf(r.showBadge));
902                        }
903                        for (NotificationChannel channel : r.channels.values()) {
904                            record.put("channel", channel.toJson());
905                        }
906                        for (NotificationChannelGroup group : r.groups.values()) {
907                            record.put("group", group.toJson());
908                        }
909                    } catch (JSONException e) {
910                        // pass
911                    }
912                    records.put(record);
913                }
914            }
915        }
916        try {
917            ranking.put("records", records);
918        } catch (JSONException e) {
919            // pass
920        }
921        return ranking;
922    }
923
924    /**
925     * Dump only the ban information as structured JSON for the stats collector.
926     *
927     * This is intentionally redundant with {#link dumpJson} because the old
928     * scraper will expect this format.
929     *
930     * @param filter
931     * @return
932     */
933    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
934        JSONArray bans = new JSONArray();
935        Map<Integer, String> packageBans = getPackageBans();
936        for(Entry<Integer, String> ban : packageBans.entrySet()) {
937            final int userId = UserHandle.getUserId(ban.getKey());
938            final String packageName = ban.getValue();
939            if (filter == null || filter.matches(packageName)) {
940                JSONObject banJson = new JSONObject();
941                try {
942                    banJson.put("userId", userId);
943                    banJson.put("packageName", packageName);
944                } catch (JSONException e) {
945                    e.printStackTrace();
946                }
947                bans.put(banJson);
948            }
949        }
950        return bans;
951    }
952
953    public Map<Integer, String> getPackageBans() {
954        synchronized (mRecords) {
955            final int N = mRecords.size();
956            ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
957            for (int i = 0; i < N; i++) {
958                final Record r = mRecords.valueAt(i);
959                if (r.importance == NotificationManager.IMPORTANCE_NONE) {
960                    packageBans.put(r.uid, r.pkg);
961                }
962            }
963
964            return packageBans;
965        }
966    }
967
968    /**
969     * Dump only the channel information as structured JSON for the stats collector.
970     *
971     * This is intentionally redundant with {#link dumpJson} because the old
972     * scraper will expect this format.
973     *
974     * @param filter
975     * @return
976     */
977    public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) {
978        JSONArray channels = new JSONArray();
979        Map<String, Integer> packageChannels = getPackageChannels();
980        for(Entry<String, Integer> channelCount : packageChannels.entrySet()) {
981            final String packageName = channelCount.getKey();
982            if (filter == null || filter.matches(packageName)) {
983                JSONObject channelCountJson = new JSONObject();
984                try {
985                    channelCountJson.put("packageName", packageName);
986                    channelCountJson.put("channelCount", channelCount.getValue());
987                } catch (JSONException e) {
988                    e.printStackTrace();
989                }
990                channels.put(channelCountJson);
991            }
992        }
993        return channels;
994    }
995
996    private Map<String, Integer> getPackageChannels() {
997        ArrayMap<String, Integer> packageChannels = new ArrayMap<>();
998        synchronized (mRecords) {
999            for (int i = 0; i < mRecords.size(); i++) {
1000                final Record r = mRecords.valueAt(i);
1001                int channelCount = 0;
1002                for (int j = 0; j < r.channels.size(); j++) {
1003                    if (!r.channels.valueAt(j).isDeleted()) {
1004                        channelCount++;
1005                    }
1006                }
1007                packageChannels.put(r.pkg, channelCount);
1008            }
1009        }
1010        return packageChannels;
1011    }
1012
1013    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
1014            int[] uidList) {
1015        if (pkgList == null || pkgList.length == 0) {
1016            return; // nothing to do
1017        }
1018        boolean updated = false;
1019        if (removingPackage) {
1020            // Remove notification settings for uninstalled package
1021            int size = Math.min(pkgList.length, uidList.length);
1022            for (int i = 0; i < size; i++) {
1023                final String pkg = pkgList[i];
1024                final int uid = uidList[i];
1025                synchronized (mRecords) {
1026                    mRecords.remove(recordKey(pkg, uid));
1027                }
1028                mRestoredWithoutUids.remove(pkg);
1029                updated = true;
1030            }
1031        } else {
1032            for (String pkg : pkgList) {
1033                // Package install
1034                final Record r = mRestoredWithoutUids.get(pkg);
1035                if (r != null) {
1036                    try {
1037                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
1038                        mRestoredWithoutUids.remove(pkg);
1039                        synchronized (mRecords) {
1040                            mRecords.put(recordKey(r.pkg, r.uid), r);
1041                        }
1042                        updated = true;
1043                    } catch (NameNotFoundException e) {
1044                        // noop
1045                    }
1046                }
1047                // Package upgrade
1048                try {
1049                    Record fullRecord = getRecord(pkg,
1050                            mPm.getPackageUidAsUser(pkg, changeUserId));
1051                    if (fullRecord != null) {
1052                        deleteDefaultChannelIfNeeded(fullRecord);
1053                    }
1054                } catch (NameNotFoundException e) {}
1055            }
1056        }
1057
1058        if (updated) {
1059            updateConfig();
1060        }
1061    }
1062
1063    private LogMaker getChannelLog(NotificationChannel channel, String pkg) {
1064        return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL)
1065                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
1066                .setPackageName(pkg)
1067                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID,
1068                        channel.getId())
1069                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE,
1070                        channel.getImportance());
1071    }
1072
1073    private static class Record {
1074        static int UNKNOWN_UID = UserHandle.USER_NULL;
1075
1076        String pkg;
1077        int uid = UNKNOWN_UID;
1078        int importance = DEFAULT_IMPORTANCE;
1079        int priority = DEFAULT_PRIORITY;
1080        int visibility = DEFAULT_VISIBILITY;
1081        boolean showBadge = DEFAULT_SHOW_BADGE;
1082
1083        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
1084        ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>();
1085   }
1086}
1087