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