RankingHelper.java revision 17717f5a6d46632cb75df78fd2a7038a4ac764ea
1/**
2 * Copyright (c) 2014, The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *     http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.server.notification;
17
18import com.android.internal.R;
19import com.android.internal.annotations.VisibleForTesting;
20import com.android.internal.logging.MetricsLogger;
21import com.android.internal.logging.nano.MetricsProto;
22import com.android.internal.util.Preconditions;
23
24import android.app.Notification;
25import android.app.NotificationChannel;
26import android.app.NotificationChannelGroup;
27import android.app.NotificationManager;
28import android.content.Context;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
31import android.content.pm.PackageManager.NameNotFoundException;
32import android.content.pm.ParceledListSlice;
33import android.metrics.LogMaker;
34import android.os.Build;
35import android.os.UserHandle;
36import android.service.notification.NotificationListenerService.Ranking;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import android.util.Slog;
40
41import org.json.JSONArray;
42import org.json.JSONException;
43import org.json.JSONObject;
44import org.xmlpull.v1.XmlPullParser;
45import org.xmlpull.v1.XmlPullParserException;
46import org.xmlpull.v1.XmlSerializer;
47
48import java.io.IOException;
49import java.io.PrintWriter;
50import java.util.ArrayList;
51import java.util.Collection;
52import java.util.Collections;
53import java.util.List;
54import java.util.Map;
55import java.util.Map.Entry;
56
57public class RankingHelper implements RankingConfig {
58    private static final String TAG = "RankingHelper";
59
60    private static final int XML_VERSION = 1;
61
62    private static final String TAG_RANKING = "ranking";
63    private static final String TAG_PACKAGE = "package";
64    private static final String TAG_CHANNEL = "channel";
65    private static final String TAG_GROUP = "channelGroup";
66
67    private static final String ATT_VERSION = "version";
68    private static final String ATT_NAME = "name";
69    private static final String ATT_UID = "uid";
70    private static final String ATT_ID = "id";
71    private static final String ATT_PRIORITY = "priority";
72    private static final String ATT_VISIBILITY = "visibility";
73    private static final String ATT_IMPORTANCE = "importance";
74    private static final String ATT_SHOW_BADGE = "show_badge";
75
76    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
77    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
78    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
79    private static final boolean DEFAULT_SHOW_BADGE = true;
80
81    private final NotificationSignalExtractor[] mSignalExtractors;
82    private final NotificationComparator mPreliminaryComparator;
83    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
84
85    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
86    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
87    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
88
89    private final Context mContext;
90    private final RankingHandler mRankingHandler;
91    private final PackageManager mPm;
92
93    public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
94            NotificationUsageStats usageStats, String[] extractorNames) {
95        mContext = context;
96        mRankingHandler = rankingHandler;
97        mPm = pm;
98
99        mPreliminaryComparator = new NotificationComparator(mContext);
100
101        final int N = extractorNames.length;
102        mSignalExtractors = new NotificationSignalExtractor[N];
103        for (int i = 0; i < N; i++) {
104            try {
105                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
106                NotificationSignalExtractor extractor =
107                        (NotificationSignalExtractor) extractorClass.newInstance();
108                extractor.initialize(mContext, usageStats);
109                extractor.setConfig(this);
110                mSignalExtractors[i] = extractor;
111            } catch (ClassNotFoundException e) {
112                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
113            } catch (InstantiationException e) {
114                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
115            } catch (IllegalAccessException e) {
116                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
117            }
118        }
119    }
120
121    @SuppressWarnings("unchecked")
122    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
123        final int N = mSignalExtractors.length;
124        for (int i = 0; i < N; i++) {
125            final NotificationSignalExtractor extractor = mSignalExtractors[i];
126            if (extractorClass.equals(extractor.getClass())) {
127                return (T) extractor;
128            }
129        }
130        return null;
131    }
132
133    public void extractSignals(NotificationRecord r) {
134        final int N = mSignalExtractors.length;
135        for (int i = 0; i < N; i++) {
136            NotificationSignalExtractor extractor = mSignalExtractors[i];
137            try {
138                RankingReconsideration recon = extractor.process(r);
139                if (recon != null) {
140                    mRankingHandler.requestReconsideration(recon);
141                }
142            } catch (Throwable t) {
143                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
144            }
145        }
146    }
147
148    public void readXml(XmlPullParser parser, boolean forRestore)
149            throws XmlPullParserException, IOException {
150        int type = parser.getEventType();
151        if (type != XmlPullParser.START_TAG) return;
152        String tag = parser.getName();
153        if (!TAG_RANKING.equals(tag)) return;
154        // Clobber groups and channels with the xml, but don't delete other data that wasn't present
155        // at the time of serialization.
156        mRestoredWithoutUids.clear();
157        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
158            tag = parser.getName();
159            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
160                return;
161            }
162            if (type == XmlPullParser.START_TAG) {
163                if (TAG_PACKAGE.equals(tag)) {
164                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
165                    String name = parser.getAttributeValue(null, ATT_NAME);
166                    if (!TextUtils.isEmpty(name)) {
167                        if (forRestore) {
168                            try {
169                                //TODO: http://b/22388012
170                                uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
171                            } catch (NameNotFoundException e) {
172                                // noop
173                            }
174                        }
175
176                        Record r = getOrCreateRecord(name, uid,
177                                safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE),
178                                safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY),
179                                safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY),
180                                safeBool(parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE));
181
182                        final int innerDepth = parser.getDepth();
183                        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
184                                && (type != XmlPullParser.END_TAG
185                                || parser.getDepth() > innerDepth)) {
186                            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
187                                continue;
188                            }
189
190                            String tagName = parser.getName();
191                            // Channel groups
192                            if (TAG_GROUP.equals(tagName)) {
193                                String id = parser.getAttributeValue(null, ATT_ID);
194                                CharSequence groupName = parser.getAttributeValue(null, ATT_NAME);
195                                if (!TextUtils.isEmpty(id)) {
196                                    NotificationChannelGroup group
197                                            = new NotificationChannelGroup(id, groupName);
198                                    r.groups.put(id, group);
199                                }
200                            }
201                            // Channels
202                            if (TAG_CHANNEL.equals(tagName)) {
203                                String id = parser.getAttributeValue(null, ATT_ID);
204                                String channelName = parser.getAttributeValue(null, ATT_NAME);
205                                int channelImportance =
206                                        safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
207                                if (!TextUtils.isEmpty(id) && !TextUtils.isEmpty(channelName)) {
208                                    NotificationChannel channel = new NotificationChannel(id,
209                                            channelName, channelImportance);
210                                    channel.populateFromXml(parser);
211                                    r.channels.put(id, channel);
212                                }
213                            }
214                        }
215
216                        try {
217                            deleteDefaultChannelIfNeeded(r);
218                        } catch (NameNotFoundException e) {
219                            Slog.e(TAG, "deleteDefaultChannelIfNeeded - Exception: " + e);
220                        }
221                    }
222                }
223            }
224        }
225        throw new IllegalStateException("Failed to reach END_DOCUMENT");
226    }
227
228    private static String recordKey(String pkg, int uid) {
229        return pkg + "|" + uid;
230    }
231
232    private Record getRecord(String pkg, int uid) {
233        final String key = recordKey(pkg, uid);
234        synchronized (mRecords) {
235            return mRecords.get(key);
236        }
237    }
238
239    private Record getOrCreateRecord(String pkg, int uid) {
240        return getOrCreateRecord(pkg, uid,
241                DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE);
242    }
243
244    private Record getOrCreateRecord(String pkg, int uid, int importance, int priority,
245            int visibility, boolean showBadge) {
246        final String key = recordKey(pkg, uid);
247        synchronized (mRecords) {
248            Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get(
249                    key);
250            if (r == null) {
251                r = new Record();
252                r.pkg = pkg;
253                r.uid = uid;
254                r.importance = importance;
255                r.priority = priority;
256                r.visibility = visibility;
257                r.showBadge = showBadge;
258
259                try {
260                    createDefaultChannelIfNeeded(r);
261                } catch (NameNotFoundException e) {
262                    Slog.e(TAG, "createDefaultChannelIfNeeded - Exception: " + e);
263                }
264
265                if (r.uid == Record.UNKNOWN_UID) {
266                    mRestoredWithoutUids.put(pkg, r);
267                } else {
268                    mRecords.put(key, r);
269                }
270            }
271            return r;
272        }
273    }
274
275    private boolean shouldHaveDefaultChannel(Record r) throws NameNotFoundException {
276        final int userId = UserHandle.getUserId(r.uid);
277        final ApplicationInfo applicationInfo = mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
278        if (applicationInfo.targetSdkVersion > Build.VERSION_CODES.N_MR1) {
279            // O apps should not have the default channel.
280            return false;
281        }
282
283        // Otherwise, this app should have the default channel.
284        return true;
285    }
286
287    private void deleteDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
288        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
289            // Not present
290            return;
291        }
292
293        if (shouldHaveDefaultChannel(r)) {
294            // Keep the default channel until upgraded.
295            return;
296        }
297
298        // Remove Default Channel.
299        r.channels.remove(NotificationChannel.DEFAULT_CHANNEL_ID);
300    }
301
302    private void createDefaultChannelIfNeeded(Record r) throws NameNotFoundException {
303        if (r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
304            r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID).setName(
305                    mContext.getString(R.string.default_notification_channel_label));
306            return;
307        }
308
309        if (!shouldHaveDefaultChannel(r)) {
310            // Keep the default channel until upgraded.
311            return;
312        }
313
314        // Create Default Channel
315        NotificationChannel channel;
316        channel = new NotificationChannel(
317                NotificationChannel.DEFAULT_CHANNEL_ID,
318                mContext.getString(R.string.default_notification_channel_label),
319                r.importance);
320        channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
321        channel.setLockscreenVisibility(r.visibility);
322        if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
323            channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
324        }
325        if (r.priority != DEFAULT_PRIORITY) {
326            channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
327        }
328        if (r.visibility != DEFAULT_VISIBILITY) {
329            channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
330        }
331        r.channels.put(channel.getId(), channel);
332    }
333
334    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
335        out.startTag(null, TAG_RANKING);
336        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
337
338        synchronized (mRecords) {
339            final int N = mRecords.size();
340            for (int i = 0; i < N; i++) {
341                final Record r = mRecords.valueAt(i);
342                //TODO: http://b/22388012
343                if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
344                    continue;
345                }
346                final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
347                        || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
348                        || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
349                        || r.groups.size() > 0;
350                if (hasNonDefaultSettings) {
351                    out.startTag(null, TAG_PACKAGE);
352                    out.attribute(null, ATT_NAME, r.pkg);
353                    if (r.importance != DEFAULT_IMPORTANCE) {
354                        out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
355                    }
356                    if (r.priority != DEFAULT_PRIORITY) {
357                        out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
358                    }
359                    if (r.visibility != DEFAULT_VISIBILITY) {
360                        out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
361                    }
362                    out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
363
364                    if (!forBackup) {
365                        out.attribute(null, ATT_UID, Integer.toString(r.uid));
366                    }
367
368                    for (NotificationChannelGroup group : r.groups.values()) {
369                        group.writeXml(out);
370                    }
371
372                    for (NotificationChannel channel : r.channels.values()) {
373                        if (!forBackup || (forBackup && !channel.isDeleted())) {
374                            channel.writeXml(out);
375                        }
376                    }
377
378                    out.endTag(null, TAG_PACKAGE);
379                }
380            }
381        }
382        out.endTag(null, TAG_RANKING);
383    }
384
385    private void updateConfig() {
386        final int N = mSignalExtractors.length;
387        for (int i = 0; i < N; i++) {
388            mSignalExtractors[i].setConfig(this);
389        }
390        mRankingHandler.requestSort(false);
391    }
392
393    public void sort(ArrayList<NotificationRecord> notificationList) {
394        final int N = notificationList.size();
395        // clear global sort keys
396        for (int i = N - 1; i >= 0; i--) {
397            notificationList.get(i).setGlobalSortKey(null);
398        }
399
400        // rank each record individually
401        Collections.sort(notificationList, mPreliminaryComparator);
402
403        synchronized (mProxyByGroupTmp) {
404            // record individual ranking result and nominate proxies for each group
405            for (int i = N - 1; i >= 0; i--) {
406                final NotificationRecord record = notificationList.get(i);
407                record.setAuthoritativeRank(i);
408                final String groupKey = record.getGroupKey();
409                NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
410                if (existingProxy == null
411                        || record.getImportance() > existingProxy.getImportance()) {
412                    mProxyByGroupTmp.put(groupKey, record);
413                }
414            }
415            // assign global sort key:
416            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
417            for (int i = 0; i < N; i++) {
418                final NotificationRecord record = notificationList.get(i);
419                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
420                String groupSortKey = record.getNotification().getSortKey();
421
422                // We need to make sure the developer provided group sort key (gsk) is handled
423                // correctly:
424                //   gsk="" < gsk=non-null-string < gsk=null
425                //
426                // We enforce this by using different prefixes for these three cases.
427                String groupSortKeyPortion;
428                if (groupSortKey == null) {
429                    groupSortKeyPortion = "nsk";
430                } else if (groupSortKey.equals("")) {
431                    groupSortKeyPortion = "esk";
432                } else {
433                    groupSortKeyPortion = "gsk=" + groupSortKey;
434                }
435
436                boolean isGroupSummary = record.getNotification().isGroupSummary();
437                record.setGlobalSortKey(
438                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
439                        record.isRecentlyIntrusive() ? '0' : '1',
440                        groupProxy.getAuthoritativeRank(),
441                        isGroupSummary ? '0' : '1',
442                        groupSortKeyPortion,
443                        record.getAuthoritativeRank()));
444            }
445            mProxyByGroupTmp.clear();
446        }
447
448        // Do a second ranking pass, using group proxies
449        Collections.sort(notificationList, mFinalComparator);
450    }
451
452    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
453        return Collections.binarySearch(notificationList, target, mFinalComparator);
454    }
455
456    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
457        final String value = parser.getAttributeValue(null, att);
458        if (TextUtils.isEmpty(value)) return defValue;
459        return Boolean.parseBoolean(value);
460    }
461
462    private static int safeInt(XmlPullParser parser, String att, int defValue) {
463        final String val = parser.getAttributeValue(null, att);
464        return tryParseInt(val, defValue);
465    }
466
467    private static int tryParseInt(String value, int defValue) {
468        if (TextUtils.isEmpty(value)) return defValue;
469        try {
470            return Integer.parseInt(value);
471        } catch (NumberFormatException e) {
472            return defValue;
473        }
474    }
475
476    /**
477     * Gets importance.
478     */
479    @Override
480    public int getImportance(String packageName, int uid) {
481        return getOrCreateRecord(packageName, uid).importance;
482    }
483
484    @Override
485    public boolean canShowBadge(String packageName, int uid) {
486        return getOrCreateRecord(packageName, uid).showBadge;
487    }
488
489    @Override
490    public void setShowBadge(String packageName, int uid, boolean showBadge) {
491        getOrCreateRecord(packageName, uid).showBadge = showBadge;
492        updateConfig();
493    }
494
495    int getPackagePriority(String pkg, int uid) {
496        return getOrCreateRecord(pkg, uid).priority;
497    }
498
499    int getPackageVisibility(String pkg, int uid) {
500        return getOrCreateRecord(pkg, uid).visibility;
501    }
502
503    @Override
504    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
505            boolean fromTargetApp) {
506        Preconditions.checkNotNull(pkg);
507        Preconditions.checkNotNull(group);
508        Preconditions.checkNotNull(group.getId());
509        Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName()));
510        Record r = getOrCreateRecord(pkg, uid);
511        if (r == null) {
512            throw new IllegalArgumentException("Invalid package");
513        }
514        LogMaker lm = new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL_GROUP)
515                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
516                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_GROUP_ID,
517                        group.getId())
518                .setPackageName(pkg);
519        MetricsLogger.action(lm);
520        r.groups.put(group.getId(), group);
521        updateConfig();
522    }
523
524    @Override
525    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
526            boolean fromTargetApp) {
527        Preconditions.checkNotNull(pkg);
528        Preconditions.checkNotNull(channel);
529        Preconditions.checkNotNull(channel.getId());
530        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName()));
531        Record r = getOrCreateRecord(pkg, uid);
532        if (r == null) {
533            throw new IllegalArgumentException("Invalid package");
534        }
535        if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
536            throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
537        }
538        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
539            throw new IllegalArgumentException("Reserved id");
540        }
541
542        NotificationChannel existing = r.channels.get(channel.getId());
543        // Keep existing settings, except deleted status and name
544        if (existing != null && fromTargetApp) {
545            if (existing.isDeleted()) {
546                existing.setDeleted(false);
547            }
548
549            existing.setName(channel.getName().toString());
550            existing.setDescription(channel.getDescription());
551
552            MetricsLogger.action(getChannelLog(channel, pkg));
553            updateConfig();
554            return;
555        }
556        if (channel.getImportance() < NotificationManager.IMPORTANCE_MIN
557                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
558            throw new IllegalArgumentException("Invalid importance level");
559        }
560        // Reset fields that apps aren't allowed to set.
561        if (fromTargetApp) {
562            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
563            channel.setLockscreenVisibility(r.visibility);
564        }
565        clearLockedFields(channel);
566        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
567            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
568        }
569        if (!r.showBadge) {
570            channel.setShowBadge(false);
571        }
572        r.channels.put(channel.getId(), channel);
573        MetricsLogger.action(getChannelLog(channel, pkg).setType(
574                MetricsProto.MetricsEvent.TYPE_OPEN));
575        updateConfig();
576    }
577
578    private void clearLockedFields(NotificationChannel channel) {
579        int clearMask = 0;
580        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
581            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
582        }
583        channel.lockFields(~clearMask);
584    }
585
586    @Override
587    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
588        Preconditions.checkNotNull(updatedChannel);
589        Preconditions.checkNotNull(updatedChannel.getId());
590        Record r = getOrCreateRecord(pkg, uid);
591        if (r == null) {
592            throw new IllegalArgumentException("Invalid package");
593        }
594        NotificationChannel channel = r.channels.get(updatedChannel.getId());
595        if (channel == null || channel.isDeleted()) {
596            throw new IllegalArgumentException("Channel does not exist");
597        }
598        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
599            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
600        }
601        r.channels.put(updatedChannel.getId(), updatedChannel);
602
603        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(updatedChannel.getId())) {
604            // copy settings to app level so they are inherited by new channels
605            // when the app migrates
606            r.importance = updatedChannel.getImportance();
607            r.priority = updatedChannel.canBypassDnd()
608                    ? Notification.PRIORITY_MAX : Notification.PRIORITY_DEFAULT;
609            r.visibility = updatedChannel.getLockscreenVisibility();
610            r.showBadge = updatedChannel.canShowBadge();
611        }
612
613        MetricsLogger.action(getChannelLog(updatedChannel, pkg));
614        updateConfig();
615    }
616
617    @Override
618    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
619            boolean includeDeleted) {
620        Preconditions.checkNotNull(pkg);
621        Record r = getOrCreateRecord(pkg, uid);
622        if (r == null) {
623            return null;
624        }
625        if (channelId == null) {
626            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
627        }
628        final NotificationChannel nc = r.channels.get(channelId);
629        if (nc != null && (includeDeleted || !nc.isDeleted())) {
630            return nc;
631        }
632        return null;
633    }
634
635    @Override
636    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
637        Record r = getRecord(pkg, uid);
638        if (r == null) {
639            return;
640        }
641        NotificationChannel channel = r.channels.get(channelId);
642        if (channel != null) {
643            channel.setDeleted(true);
644            LogMaker lm = getChannelLog(channel, pkg);
645            lm.setType(MetricsProto.MetricsEvent.TYPE_CLOSE);
646            MetricsLogger.action(lm);
647            updateConfig();
648        }
649    }
650
651    @Override
652    @VisibleForTesting
653    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
654        Preconditions.checkNotNull(pkg);
655        Preconditions.checkNotNull(channelId);
656        Record r = getRecord(pkg, uid);
657        if (r == null) {
658            return;
659        }
660        r.channels.remove(channelId);
661        updateConfig();
662    }
663
664    @Override
665    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
666        Preconditions.checkNotNull(pkg);
667        Record r = getRecord(pkg, uid);
668        if (r == null) {
669            return;
670        }
671        int N = r.channels.size() - 1;
672        for (int i = N; i >= 0; i--) {
673            String key = r.channels.keyAt(i);
674            if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) {
675                r.channels.remove(key);
676            }
677        }
678        updateConfig();
679    }
680
681    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
682            int uid) {
683        Preconditions.checkNotNull(pkg);
684        Record r = getRecord(pkg, uid);
685        return r.groups.get(groupId);
686    }
687
688    @Override
689    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
690            int uid, boolean includeDeleted) {
691        Preconditions.checkNotNull(pkg);
692        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
693        Record r = getRecord(pkg, uid);
694        if (r == null) {
695            return ParceledListSlice.emptyList();
696        }
697        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
698        int N = r.channels.size();
699        for (int i = 0; i < N; i++) {
700            final NotificationChannel nc = r.channels.valueAt(i);
701            if (includeDeleted || !nc.isDeleted()) {
702                if (nc.getGroup() != null) {
703                    if (r.groups.get(nc.getGroup()) != null) {
704                        NotificationChannelGroup ncg = groups.get(nc.getGroup());
705                        if (ncg == null) {
706                            ncg = r.groups.get(nc.getGroup()).clone();
707                            groups.put(nc.getGroup(), ncg);
708
709                        }
710                        ncg.addChannel(nc);
711                    }
712                } else {
713                    nonGrouped.addChannel(nc);
714                }
715            }
716        }
717        if (nonGrouped.getChannels().size() > 0) {
718            groups.put(null, nonGrouped);
719        }
720        return new ParceledListSlice<>(new ArrayList<>(groups.values()));
721    }
722
723    public List<NotificationChannel> deleteNotificationChannelGroup(String pkg, int uid,
724            String groupId) {
725        List<NotificationChannel> deletedChannels = new ArrayList<>();
726        Record r = getRecord(pkg, uid);
727        if (r == null || TextUtils.isEmpty(groupId)) {
728            return deletedChannels;
729        }
730
731        r.groups.remove(groupId);
732
733        int N = r.channels.size();
734        for (int i = 0; i < N; i++) {
735            final NotificationChannel nc = r.channels.valueAt(i);
736            if (groupId.equals(nc.getGroup())) {
737                nc.setDeleted(true);
738                deletedChannels.add(nc);
739            }
740        }
741        updateConfig();
742        return deletedChannels;
743    }
744
745    @Override
746    public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
747            int uid) {
748        Record r = getRecord(pkg, uid);
749        if (r == null) {
750            return new ArrayList<>();
751        }
752        return r.groups.values();
753    }
754
755    @Override
756    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
757            boolean includeDeleted) {
758        Preconditions.checkNotNull(pkg);
759        List<NotificationChannel> channels = new ArrayList<>();
760        Record r = getRecord(pkg, uid);
761        if (r == null) {
762            return ParceledListSlice.emptyList();
763        }
764        int N = r.channels.size();
765        for (int i = 0; i < N; i++) {
766            final NotificationChannel nc = r.channels.valueAt(i);
767            if (includeDeleted || !nc.isDeleted()) {
768                channels.add(nc);
769            }
770        }
771        return new ParceledListSlice<>(channels);
772    }
773
774    /**
775     * True for pre-O apps that only have the default channel, or pre O apps that have no
776     * channels yet. This method will create the default channel for pre-O apps that don't have it.
777     * Should never be true for O+ targeting apps, but that's enforced on boot/when an app
778     * upgrades.
779     */
780    public boolean onlyHasDefaultChannel(String pkg, int uid) {
781        Record r = getOrCreateRecord(pkg, uid);
782        if (r.channels.size() == 1
783                && r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
784            return true;
785        }
786        return false;
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 onUserRemoved(int userId) {
1031        synchronized (mRecords) {
1032            int N = mRecords.size();
1033            for (int i = N - 1; i >= 0 ; i--) {
1034                Record record = mRecords.valueAt(i);
1035                if (UserHandle.getUserId(record.uid) == userId) {
1036                    mRecords.removeAt(i);
1037                }
1038            }
1039        }
1040    }
1041
1042    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
1043            int[] uidList) {
1044        if (pkgList == null || pkgList.length == 0) {
1045            return; // nothing to do
1046        }
1047        boolean updated = false;
1048        if (removingPackage) {
1049            // Remove notification settings for uninstalled package
1050            int size = Math.min(pkgList.length, uidList.length);
1051            for (int i = 0; i < size; i++) {
1052                final String pkg = pkgList[i];
1053                final int uid = uidList[i];
1054                synchronized (mRecords) {
1055                    mRecords.remove(recordKey(pkg, uid));
1056                }
1057                mRestoredWithoutUids.remove(pkg);
1058                updated = true;
1059            }
1060        } else {
1061            for (String pkg : pkgList) {
1062                // Package install
1063                final Record r = mRestoredWithoutUids.get(pkg);
1064                if (r != null) {
1065                    try {
1066                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
1067                        mRestoredWithoutUids.remove(pkg);
1068                        synchronized (mRecords) {
1069                            mRecords.put(recordKey(r.pkg, r.uid), r);
1070                        }
1071                        updated = true;
1072                    } catch (NameNotFoundException e) {
1073                        // noop
1074                    }
1075                }
1076                // Package upgrade
1077                try {
1078                    Record fullRecord = getRecord(pkg,
1079                            mPm.getPackageUidAsUser(pkg, changeUserId));
1080                    if (fullRecord != null) {
1081                        deleteDefaultChannelIfNeeded(fullRecord);
1082                    }
1083                } catch (NameNotFoundException e) {}
1084            }
1085        }
1086
1087        if (updated) {
1088            updateConfig();
1089        }
1090    }
1091
1092    private LogMaker getChannelLog(NotificationChannel channel, String pkg) {
1093        return new LogMaker(MetricsProto.MetricsEvent.ACTION_NOTIFICATION_CHANNEL)
1094                .setType(MetricsProto.MetricsEvent.TYPE_UPDATE)
1095                .setPackageName(pkg)
1096                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_ID,
1097                        channel.getId())
1098                .addTaggedData(MetricsProto.MetricsEvent.FIELD_NOTIFICATION_CHANNEL_IMPORTANCE,
1099                        channel.getImportance());
1100    }
1101
1102    private static class Record {
1103        static int UNKNOWN_UID = UserHandle.USER_NULL;
1104
1105        String pkg;
1106        int uid = UNKNOWN_UID;
1107        int importance = DEFAULT_IMPORTANCE;
1108        int priority = DEFAULT_PRIORITY;
1109        int visibility = DEFAULT_VISIBILITY;
1110        boolean showBadge = DEFAULT_SHOW_BADGE;
1111
1112        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
1113        ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>();
1114   }
1115}
1116