RankingHelper.java revision fa04e143d833ab28b50752ec7c91c2458b7ad5a4
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            // Pre-O apps should have it.
280            return true;
281        }
282
283        // STOPSHIP TODO: remove before release - O+ apps should never have a default channel.
284        // But for now, leave the default channel until an app has created its first channel.
285        boolean hasCreatedAChannel = false;
286        final int size = r.channels.size();
287        for (int i = 0; i < size; i++) {
288            final NotificationChannel notificationChannel = r.channels.valueAt(i);
289            if (notificationChannel != null &&
290                    !notificationChannel.getId().equals(NotificationChannel.DEFAULT_CHANNEL_ID)) {
291                hasCreatedAChannel = true;
292                break;
293            }
294        }
295        if (!hasCreatedAChannel) {
296            return true;
297        }
298
299        // Otherwise, should not have the default channel.
300        return false;
301    }
302
303    private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
304        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
305            // Not present
306            return;
307        }
308
309        if (shouldHaveDefaultChannel(r)) {
310            // Keep the default channel until upgraded.
311            return;
312        }
313
314        // Remove Default Channel.
315        r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID);
316    }
317
318    private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
319        if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
320            // Already exists
321            return;
322        }
323
324        if (!shouldHaveDefaultChannel(r)) {
325            // Keep the default channel until upgraded.
326            return;
327        }
328
329        // Create Default Channel
330        NotificationChannel channel;
331        channel = new NotificationChannel(
332                NotificationChannel.DEFAULT_CHANNEL_ID,
333                mContext.getString(R.string.default_notification_channel_label),
334                r.importance);
335        channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
336        channel.setLockscreenVisibility(r.visibility);
337        if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
338            channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
339        }
340        if (r.priority != DEFAULT_PRIORITY) {
341            channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
342        }
343        if (r.visibility != DEFAULT_VISIBILITY) {
344            channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
345        }
346        r.channels.put(channel.getId(), channel);
347    }
348
349    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
350        out.startTag(null, TAG_RANKING);
351        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
352
353        synchronized (mRecords) {
354            final int N = mRecords.size();
355            for (int i = 0; i < N; i++) {
356                final Record r = mRecords.valueAt(i);
357                //TODO: http://b/22388012
358                if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
359                    continue;
360                }
361                final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
362                        || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
363                        || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
364                        || r.groups.size() > 0;
365                if (hasNonDefaultSettings) {
366                    out.startTag(null, TAG_PACKAGE);
367                    out.attribute(null, ATT_NAME, r.pkg);
368                    if (r.importance != DEFAULT_IMPORTANCE) {
369                        out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
370                    }
371                    if (r.priority != DEFAULT_PRIORITY) {
372                        out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
373                    }
374                    if (r.visibility != DEFAULT_VISIBILITY) {
375                        out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
376                    }
377                    out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
378
379                    if (!forBackup) {
380                        out.attribute(null, ATT_UID, Integer.toString(r.uid));
381                    }
382
383                    for (NotificationChannelGroup group : r.groups.values()) {
384                        group.writeXml(out);
385                    }
386
387                    for (NotificationChannel channel : r.channels.values()) {
388                        if (!forBackup || (forBackup && !channel.isDeleted())) {
389                            channel.writeXml(out);
390                        }
391                    }
392
393                    out.endTag(null, TAG_PACKAGE);
394                }
395            }
396        }
397        out.endTag(null, TAG_RANKING);
398    }
399
400    private void updateConfig() {
401        final int N = mSignalExtractors.length;
402        for (int i = 0; i < N; i++) {
403            mSignalExtractors[i].setConfig(this);
404        }
405        mRankingHandler.requestSort(false);
406    }
407
408    public void sort(ArrayList<NotificationRecord> notificationList) {
409        final int N = notificationList.size();
410        // clear global sort keys
411        for (int i = N - 1; i >= 0; i--) {
412            notificationList.get(i).setGlobalSortKey(null);
413        }
414
415        // rank each record individually
416        Collections.sort(notificationList, mPreliminaryComparator);
417
418        synchronized (mProxyByGroupTmp) {
419            // record individual ranking result and nominate proxies for each group
420            for (int i = N - 1; i >= 0; i--) {
421                final NotificationRecord record = notificationList.get(i);
422                record.setAuthoritativeRank(i);
423                final String groupKey = record.getGroupKey();
424                NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
425                if (existingProxy == null
426                        || record.getImportance() > existingProxy.getImportance()) {
427                    mProxyByGroupTmp.put(groupKey, record);
428                }
429            }
430            // assign global sort key:
431            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
432            for (int i = 0; i < N; i++) {
433                final NotificationRecord record = notificationList.get(i);
434                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
435                String groupSortKey = record.getNotification().getSortKey();
436
437                // We need to make sure the developer provided group sort key (gsk) is handled
438                // correctly:
439                //   gsk="" < gsk=non-null-string < gsk=null
440                //
441                // We enforce this by using different prefixes for these three cases.
442                String groupSortKeyPortion;
443                if (groupSortKey == null) {
444                    groupSortKeyPortion = "nsk";
445                } else if (groupSortKey.equals("")) {
446                    groupSortKeyPortion = "esk";
447                } else {
448                    groupSortKeyPortion = "gsk=" + groupSortKey;
449                }
450
451                boolean isGroupSummary = record.getNotification().isGroupSummary();
452                record.setGlobalSortKey(
453                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
454                        record.isRecentlyIntrusive() ? '0' : '1',
455                        groupProxy.getAuthoritativeRank(),
456                        isGroupSummary ? '0' : '1',
457                        groupSortKeyPortion,
458                        record.getAuthoritativeRank()));
459            }
460            mProxyByGroupTmp.clear();
461        }
462
463        // Do a second ranking pass, using group proxies
464        Collections.sort(notificationList, mFinalComparator);
465    }
466
467    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
468        return Collections.binarySearch(notificationList, target, mFinalComparator);
469    }
470
471    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
472        final String value = parser.getAttributeValue(null, att);
473        if (TextUtils.isEmpty(value)) return defValue;
474        return Boolean.parseBoolean(value);
475    }
476
477    private static int safeInt(XmlPullParser parser, String att, int defValue) {
478        final String val = parser.getAttributeValue(null, att);
479        return tryParseInt(val, defValue);
480    }
481
482    private static int tryParseInt(String value, int defValue) {
483        if (TextUtils.isEmpty(value)) return defValue;
484        try {
485            return Integer.parseInt(value);
486        } catch (NumberFormatException e) {
487            return defValue;
488        }
489    }
490
491    /**
492     * Gets importance.
493     */
494    @Override
495    public int getImportance(String packageName, int uid) {
496        return getOrCreateRecord(packageName, uid).importance;
497    }
498
499    @Override
500    public boolean canShowBadge(String packageName, int uid) {
501        return getOrCreateRecord(packageName, uid).showBadge;
502    }
503
504    @Override
505    public void setShowBadge(String packageName, int uid, boolean showBadge) {
506        getOrCreateRecord(packageName, uid).showBadge = showBadge;
507        updateConfig();
508    }
509
510    int getPackagePriority(String pkg, int uid) {
511        return getOrCreateRecord(pkg, uid).priority;
512    }
513
514    int getPackageVisibility(String pkg, int uid) {
515        return getOrCreateRecord(pkg, uid).visibility;
516    }
517
518    @Override
519    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
520            boolean fromTargetApp) {
521        Preconditions.checkNotNull(pkg);
522        Preconditions.checkNotNull(group);
523        Preconditions.checkNotNull(group.getId());
524        Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName()));
525        Record r = getOrCreateRecord(pkg, uid);
526        if (r == null) {
527            throw new IllegalArgumentException("Invalid package");
528        }
529        LogMaker lm = new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP)
530                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
531                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID,
532                        group.getId())
533                .setPackageName(pkg);
534        MetricsLogger.action(lm);
535        r.groups.put(group.getId(), group);
536        updateConfig();
537    }
538
539    @Override
540    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
541            boolean fromTargetApp) {
542        Preconditions.checkNotNull(pkg);
543        Preconditions.checkNotNull(channel);
544        Preconditions.checkNotNull(channel.getId());
545        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
546        Record r = getOrCreateRecord(pkg, uid);
547        if (r == null) {
548            throw new IllegalArgumentException("Invalid package");
549        }
550        if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
551            throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
552        }
553        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
554            throw new IllegalArgumentException("Reserved id");
555        }
556
557        NotificationChannel existing = r.channels.get(channel.getId());
558        // Keep existing settings, except deleted status and name
559        if (existing != null && fromTargetApp) {
560            if (existing.isDeleted()) {
561                existing.setDeleted(false);
562            }
563
564            existing.setName(channel.getName().toString());
565            existing.setDescription(channel.getDescription());
566
567            MetricsLogger.action(getChannelLog(channel, pkg));
568            updateConfig();
569            return;
570        }
571        if (channel.getImportance() < NotificationManager.IMPORTANCE_MIN
572                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
573            throw new IllegalArgumentException("Invalid importance level");
574        }
575        // Reset fields that apps aren't allowed to set.
576        if (fromTargetApp) {
577            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
578            channel.setLockscreenVisibility(r.visibility);
579        }
580        clearLockedFields(channel);
581        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
582            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
583        }
584        if (!r.showBadge) {
585            channel.setShowBadge(false);
586        }
587        r.channels.put(channel.getId(), channel);
588        MetricsLogger.action(getChannelLog(channel, pkg).setType(
589                MetricsProto.MetricsEvent.TYPE_OPEN));
590        updateConfig();
591    }
592
593    private void clearLockedFields(NotificationChannel channel) {
594        int clearMask = 0;
595        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
596            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
597        }
598        channel.lockFields(~clearMask);
599    }
600
601    @Override
602    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
603        Preconditions.checkNotNull(updatedChannel);
604        Preconditions.checkNotNull(updatedChannel.getId());
605        Record r = getOrCreateRecord(pkg, uid);
606        if (r == null) {
607            throw new IllegalArgumentException("Invalid package");
608        }
609        NotificationChannel channel = r.channels.get(updatedChannel.getId());
610        if (channel == null || channel.isDeleted()) {
611            throw new IllegalArgumentException("Channel does not exist");
612        }
613        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
614            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
615        }
616        r.channels.put(updatedChannel.getId(), updatedChannel);
617
618        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(updatedChannel.getId())) {
619            // copy settings to app level so they are inherited by new channels
620            // when the app migrates
621            r.importance = updatedChannel.getImportance();
622            r.priority = updatedChannel.canBypassDnd()
623                    ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT;
624            r.visibility = updatedChannel.getLockscreenVisibility();
625            r.showBadge = updatedChannel.canShowBadge();
626        }
627
628        MetricsLogger.action(getChannelLog(updatedChannel, pkg));
629        updateConfig();
630    }
631
632    @Override
633    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
634            boolean includeDeleted) {
635        Preconditions.checkNotNull(pkg);
636        Record r = getOrCreateRecord(pkg, uid);
637        if (r == null) {
638            return null;
639        }
640        if (channelId == null) {
641            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
642        }
643        final NotificationChannel nc = r.channels.get(channelId);
644        if (nc != null && (includeDeleted || !nc.isDeleted())) {
645            return nc;
646        }
647        return null;
648    }
649
650    @Override
651    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
652        Record r = getRecord(pkg, uid);
653        if (r == null) {
654            return;
655        }
656        NotificationChannel channel = r.channels.get(channelId);
657        if (channel != null) {
658            channel.setDeleted(true);
659            LogMaker lm = getChannelLog(channel, pkg);
660            lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE);
661            MetricsLogger.action(lm);
662            updateConfig();
663        }
664    }
665
666    @Override
667    @VisibleForTesting
668    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
669        Preconditions.checkNotNull(pkg);
670        Preconditions.checkNotNull(channelId);
671        Record r = getRecord(pkg, uid);
672        if (r == null) {
673            return;
674        }
675        r.channels.remove(channelId);
676        updateConfig();
677    }
678
679    @Override
680    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
681        Preconditions.checkNotNull(pkg);
682        Record r = getRecord(pkg, uid);
683        if (r == null) {
684            return;
685        }
686        int N = r.channels.size() - 1;
687        for (int i = N; i >= 0; i--) {
688            String key = r.channels.keyAt(i);
689            if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) {
690                r.channels.remove(key);
691            }
692        }
693        updateConfig();
694    }
695
696    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
697            int uid) {
698        Preconditions.checkNotNull(pkg);
699        Record r = getRecord(pkg, uid);
700        return r.groups.get(groupId);
701    }
702
703    @Override
704    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
705            int uid, boolean includeDeleted) {
706        Preconditions.checkNotNull(pkg);
707        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
708        Record r = getRecord(pkg, uid);
709        if (r == null) {
710            return ParceledListSlice.emptyList();
711        }
712        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
713        int N = r.channels.size();
714        for (int i = 0; i < N; i++) {
715            final NotificationChannel nc = r.channels.valueAt(i);
716            if (includeDeleted || !nc.isDeleted()) {
717                if (nc.getGroup() != null) {
718                    if (r.groups.get(nc.getGroup()) != null) {
719                        NotificationChannelGroup ncg = groups.get(nc.getGroup());
720                        if (ncg == null) {
721                            ncg = r.groups.get(nc.getGroup()).clone();
722                            groups.put(nc.getGroup(), ncg);
723
724                        }
725                        ncg.addChannel(nc);
726                    }
727                } else {
728                    nonGrouped.addChannel(nc);
729                }
730            }
731        }
732        if (nonGrouped.getChannels().size() > 0) {
733            groups.put(null, nonGrouped);
734        }
735        return new ParceledListSlice<>(new ArrayList<>(groups.values()));
736    }
737
738    public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid,
739            String groupId) {
740        List<NotificationChannel> deletedChannels = new ArrayList<>();
741        Record r = getRecord(pkg, uid);
742        if (r == null || TextUtils.isEmpty(groupId)) {
743            return deletedChannels;
744        }
745
746        r.groups.remove(groupId);
747
748        int N = r.channels.size();
749        for (int i = 0; i < N; i++) {
750            final NotificationChannel nc = r.channels.valueAt(i);
751            if (groupId.equals(nc.getGroup())) {
752                nc.setDeleted(true);
753                deletedChannels.add(nc);
754            }
755        }
756        updateConfig();
757        return deletedChannels;
758    }
759
760    @Override
761    public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
762            int uid) {
763        Record r = getRecord(pkg, uid);
764        if (r == null) {
765            return new ArrayList<>();
766        }
767        return r.groups.values();
768    }
769
770    @Override
771    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
772            boolean includeDeleted) {
773        Preconditions.checkNotNull(pkg);
774        List<NotificationChannel> channels = new ArrayList<>();
775        Record r = getRecord(pkg, uid);
776        if (r == null) {
777            return ParceledListSlice.emptyList();
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 (includeDeleted || !nc.isDeleted()) {
783                channels.add(nc);
784            }
785        }
786        return new ParceledListSlice<>(channels);
787    }
788
789    public int getDeletedChannelCount(String pkg, int uid) {
790        Preconditions.checkNotNull(pkg);
791        int deletedCount = 0;
792        Record r = getRecord(pkg, uid);
793        if (r == null) {
794            return deletedCount;
795        }
796        int N = r.channels.size();
797        for (int i = 0; i < N; i++) {
798            final NotificationChannel nc = r.channels.valueAt(i);
799            if (nc.isDeleted()) {
800                deletedCount++;
801            }
802        }
803        return deletedCount;
804    }
805
806    /**
807     * Sets importance.
808     */
809    @Override
810    public void setImportance(String pkgName, int uid, int importance) {
811        getOrCreateRecord(pkgName, uid).importance = importance;
812        updateConfig();
813    }
814
815    public void setEnabled(String packageName, int uid, boolean enabled) {
816        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
817        if (wasEnabled == enabled) {
818            return;
819        }
820        setImportance(packageName, uid,
821                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
822    }
823
824    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
825        if (filter == null) {
826            final int N = mSignalExtractors.length;
827            pw.print(prefix);
828            pw.print("mSignalExtractors.length = ");
829            pw.println(N);
830            for (int i = 0; i < N; i++) {
831                pw.print(prefix);
832                pw.print("  ");
833                pw.println(mSignalExtractors[i]);
834            }
835        }
836        if (filter == null) {
837            pw.print(prefix);
838            pw.println("per-package config:");
839        }
840        pw.println("Records:");
841        synchronized (mRecords) {
842            dumpRecords(pw, prefix, filter, mRecords);
843        }
844        pw.println("Restored without uid:");
845        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
846    }
847
848    private static void dumpRecords(PrintWriter pw, String prefix,
849            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
850        final int N = records.size();
851        for (int i = 0; i < N; i++) {
852            final Record r = records.valueAt(i);
853            if (filter == null || filter.matches(r.pkg)) {
854                pw.print(prefix);
855                pw.print("  AppSettings: ");
856                pw.print(r.pkg);
857                pw.print(" (");
858                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
859                pw.print(')');
860                if (r.importance != DEFAULT_IMPORTANCE) {
861                    pw.print(" importance=");
862                    pw.print(Ranking.importanceToString(r.importance));
863                }
864                if (r.priority != DEFAULT_PRIORITY) {
865                    pw.print(" priority=");
866                    pw.print(Notification.priorityToString(r.priority));
867                }
868                if (r.visibility != DEFAULT_VISIBILITY) {
869                    pw.print(" visibility=");
870                    pw.print(Notification.visibilityToString(r.visibility));
871                }
872                pw.print(" showBadge=");
873                pw.print(Boolean.toString(r.showBadge));
874                pw.println();
875                for (NotificationChannel channel : r.channels.values()) {
876                    pw.print(prefix);
877                    pw.print("  ");
878                    pw.print("  ");
879                    pw.println(channel);
880                }
881                for (NotificationChannelGroup group : r.groups.values()) {
882                    pw.print(prefix);
883                    pw.print("  ");
884                    pw.print("  ");
885                    pw.println(group);
886                }
887            }
888        }
889    }
890
891    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
892        JSONObject ranking = new JSONObject();
893        JSONArray records = new JSONArray();
894        try {
895            ranking.put("noUid", mRestoredWithoutUids.size());
896        } catch (JSONException e) {
897           // pass
898        }
899        synchronized (mRecords) {
900            final int N = mRecords.size();
901            for (int i = 0; i < N; i++) {
902                final Record r = mRecords.valueAt(i);
903                if (filter == null || filter.matches(r.pkg)) {
904                    JSONObject record = new JSONObject();
905                    try {
906                        record.put("userId", UserHandle.getUserId(r.uid));
907                        record.put("packageName", r.pkg);
908                        if (r.importance != DEFAULT_IMPORTANCE) {
909                            record.put("importance", Ranking.importanceToString(r.importance));
910                        }
911                        if (r.priority != DEFAULT_PRIORITY) {
912                            record.put("priority", Notification.priorityToString(r.priority));
913                        }
914                        if (r.visibility != DEFAULT_VISIBILITY) {
915                            record.put("visibility", Notification.visibilityToString(r.visibility));
916                        }
917                        if (r.showBadge != DEFAULT_SHOW_BADGE) {
918                            record.put("showBadge", Boolean.valueOf(r.showBadge));
919                        }
920                        for (NotificationChannel channel : r.channels.values()) {
921                            record.put("channel", channel.toJson());
922                        }
923                        for (NotificationChannelGroup group : r.groups.values()) {
924                            record.put("group", group.toJson());
925                        }
926                    } catch (JSONException e) {
927                        // pass
928                    }
929                    records.put(record);
930                }
931            }
932        }
933        try {
934            ranking.put("records", records);
935        } catch (JSONException e) {
936            // pass
937        }
938        return ranking;
939    }
940
941    /**
942     * Dump only the ban information as structured JSON for the stats collector.
943     *
944     * This is intentionally redundant with {#link dumpJson} because the old
945     * scraper will expect this format.
946     *
947     * @param filter
948     * @return
949     */
950    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
951        JSONArray bans = new JSONArray();
952        Map<Integer, String> packageBans = getPackageBans();
953        for(Entry<Integer, String> ban : packageBans.entrySet()) {
954            final int userId = UserHandle.getUserId(ban.getKey());
955            final String packageName = ban.getValue();
956            if (filter == null || filter.matches(packageName)) {
957                JSONObject banJson = new JSONObject();
958                try {
959                    banJson.put("userId", userId);
960                    banJson.put("packageName", packageName);
961                } catch (JSONException e) {
962                    e.printStackTrace();
963                }
964                bans.put(banJson);
965            }
966        }
967        return bans;
968    }
969
970    public Map<Integer, String> getPackageBans() {
971        synchronized (mRecords) {
972            final int N = mRecords.size();
973            ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
974            for (int i = 0; i < N; i++) {
975                final Record r = mRecords.valueAt(i);
976                if (r.importance == NotificationManager.IMPORTANCE_NONE) {
977                    packageBans.put(r.uid, r.pkg);
978                }
979            }
980
981            return packageBans;
982        }
983    }
984
985    /**
986     * Dump only the channel information as structured JSON for the stats collector.
987     *
988     * This is intentionally redundant with {#link dumpJson} because the old
989     * scraper will expect this format.
990     *
991     * @param filter
992     * @return
993     */
994    public JSONArray dumpChannelsJson(NotificationManagerService.DumpFilter filter) {
995        JSONArray channels = new JSONArray();
996        Map<String, Integer> packageChannels = getPackageChannels();
997        for(Entry<String, Integer> channelCount : packageChannels.entrySet()) {
998            final String packageName = channelCount.getKey();
999            if (filter == null || filter.matches(packageName)) {
1000                JSONObject channelCountJson = new JSONObject();
1001                try {
1002                    channelCountJson.put("packageName", packageName);
1003                    channelCountJson.put("channelCount", channelCount.getValue());
1004                } catch (JSONException e) {
1005                    e.printStackTrace();
1006                }
1007                channels.put(channelCountJson);
1008            }
1009        }
1010        return channels;
1011    }
1012
1013    private Map<String, Integer> getPackageChannels() {
1014        ArrayMap<String, Integer> packageChannels = new ArrayMap<>();
1015        synchronized (mRecords) {
1016            for (int i = 0; i < mRecords.size(); i++) {
1017                final Record r = mRecords.valueAt(i);
1018                int channelCount = 0;
1019                for (int j = 0; j < r.channels.size(); j++) {
1020                    if (!r.channels.valueAt(j).isDeleted()) {
1021                        channelCount++;
1022                    }
1023                }
1024                packageChannels.put(r.pkg, channelCount);
1025            }
1026        }
1027        return packageChannels;
1028    }
1029
1030    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
1031            int[] uidList) {
1032        if (pkgList == null || pkgList.length == 0) {
1033            return; // nothing to do
1034        }
1035        boolean updated = false;
1036        if (removingPackage) {
1037            // Remove notification settings for uninstalled package
1038            int size = Math.min(pkgList.length, uidList.length);
1039            for (int i = 0; i < size; i++) {
1040                final String pkg = pkgList[i];
1041                final int uid = uidList[i];
1042                synchronized (mRecords) {
1043                    mRecords.remove(recordKey(pkg, uid));
1044                }
1045                mRestoredWithoutUids.remove(pkg);
1046                updated = true;
1047            }
1048        } else {
1049            for (String pkg : pkgList) {
1050                // Package install
1051                final Record r = mRestoredWithoutUids.get(pkg);
1052                if (r != null) {
1053                    try {
1054                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
1055                        mRestoredWithoutUids.remove(pkg);
1056                        synchronized (mRecords) {
1057                            mRecords.put(recordKey(r.pkg, r.uid), r);
1058                        }
1059                        updated = true;
1060                    } catch (NameNotFoundException e) {
1061                        // noop
1062                    }
1063                }
1064                // Package upgrade
1065                try {
1066                    Record fullRecord = getRecord(pkg,
1067                            mPm.getPackageUidAsUser(pkg, changeUserId));
1068                    if (fullRecord != null) {
1069                        deleteDefaultChannelIfNeeded(fullRecord);
1070                    }
1071                } catch (NameNotFoundException e) {}
1072            }
1073        }
1074
1075        if (updated) {
1076            updateConfig();
1077        }
1078    }
1079
1080    private LogMaker getChannelLog(NotificationChannel channel, String pkg) {
1081        return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL)
1082                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
1083                .setPackageName(pkg)
1084                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID,
1085                        channel.getId())
1086                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE,
1087                        channel.getImportance());
1088    }
1089
1090    private static class Record {
1091        static int UNKNOWN_UID = UserHandle.USER_NULL;
1092
1093        String pkg;
1094        int uid = UNKNOWN_UID;
1095        int importance = DEFAULT_IMPORTANCE;
1096        int priority = DEFAULT_PRIORITY;
1097        int visibility = DEFAULT_VISIBILITY;
1098        boolean showBadge = DEFAULT_SHOW_BADGE;
1099
1100        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
1101        ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>();
1102   }
1103}
1104