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