RankingHelper.java revision 5a31193497fa923e24bc5c9796b40fdfae604d31
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 static android.app.NotificationManager.IMPORTANCE_NONE;
19
20import com.android.internal.R;
21import com.android.internal.annotations.VisibleForTesting;
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.os.Build;
34import android.os.UserHandle;
35import android.provider.Settings;
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_NAME_RES_ID = "name_res_id";
70    private static final String ATT_UID = "uid";
71    private static final String ATT_ID = "id";
72    private static final String ATT_PRIORITY = "priority";
73    private static final String ATT_VISIBILITY = "visibility";
74    private static final String ATT_IMPORTANCE = "importance";
75    private static final String ATT_SHOW_BADGE = "show_badge";
76
77    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
78    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
79    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
80    private static final boolean DEFAULT_SHOW_BADGE = true;
81
82    private final NotificationSignalExtractor[] mSignalExtractors;
83    private final NotificationComparator mPreliminaryComparator;
84    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
85
86    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
87    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
88    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
89
90    private final Context mContext;
91    private final RankingHandler mRankingHandler;
92    private final PackageManager mPm;
93
94    public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
95            NotificationUsageStats usageStats, String[] extractorNames) {
96        mContext = context;
97        mRankingHandler = rankingHandler;
98        mPm = pm;
99
100        mPreliminaryComparator = new NotificationComparator(mContext);
101
102        final int N = extractorNames.length;
103        mSignalExtractors = new NotificationSignalExtractor[N];
104        for (int i = 0; i < N; i++) {
105            try {
106                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
107                NotificationSignalExtractor extractor =
108                        (NotificationSignalExtractor) extractorClass.newInstance();
109                extractor.initialize(mContext, usageStats);
110                extractor.setConfig(this);
111                mSignalExtractors[i] = extractor;
112            } catch (ClassNotFoundException e) {
113                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
114            } catch (InstantiationException e) {
115                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
116            } catch (IllegalAccessException e) {
117                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
118            }
119        }
120    }
121
122    @SuppressWarnings("unchecked")
123    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
124        final int N = mSignalExtractors.length;
125        for (int i = 0; i < N; i++) {
126            final NotificationSignalExtractor extractor = mSignalExtractors[i];
127            if (extractorClass.equals(extractor.getClass())) {
128                return (T) extractor;
129            }
130        }
131        return null;
132    }
133
134    public void extractSignals(NotificationRecord r) {
135        final int N = mSignalExtractors.length;
136        for (int i = 0; i < N; i++) {
137            NotificationSignalExtractor extractor = mSignalExtractors[i];
138            try {
139                RankingReconsideration recon = extractor.process(r);
140                if (recon != null) {
141                    mRankingHandler.requestReconsideration(recon);
142                }
143            } catch (Throwable t) {
144                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
145            }
146        }
147    }
148
149    public void readXml(XmlPullParser parser, boolean forRestore)
150            throws XmlPullParserException, IOException {
151        int type = parser.getEventType();
152        if (type != XmlPullParser.START_TAG) return;
153        String tag = parser.getName();
154        if (!TAG_RANKING.equals(tag)) return;
155        mRecords.clear();
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                                    final 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                                CharSequence channelName = parser.getAttributeValue(null, ATT_NAME);
205                                int channelNameRes = safeInt(parser, ATT_NAME_RES_ID, -1);
206                                int channelImportance =
207                                        safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
208
209                                if (!TextUtils.isEmpty(id)) {
210                                    NotificationChannel channel;
211                                    if (channelName != null) {
212                                        channel = new NotificationChannel(id, channelName,
213                                                channelImportance);
214                                    } else {
215                                        channel = new NotificationChannel(id, channelNameRes,
216                                                channelImportance);
217                                    }
218                                    channel.populateFromXml(parser);
219                                    r.channels.put(id, channel);
220                                }
221                            }
222                        }
223
224                        clampDefaultChannel(r);
225                    }
226                }
227            }
228        }
229        throw new IllegalStateException("Failed to reach END_DOCUMENT");
230    }
231
232    private static String recordKey(String pkg, int uid) {
233        return pkg + "|" + uid;
234    }
235
236    private Record getRecord(String pkg, int uid) {
237        final String key = recordKey(pkg, uid);
238        return mRecords.get(key);
239    }
240
241    private Record getOrCreateRecord(String pkg, int uid) {
242        return getOrCreateRecord(pkg, uid,
243                DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE);
244    }
245
246    private Record getOrCreateRecord(String pkg, int uid, int importance, int priority,
247            int visibility, boolean showBadge) {
248        final String key = recordKey(pkg, uid);
249        Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get(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            createDefaultChannelIfMissing(r);
259            if (r.uid == Record.UNKNOWN_UID) {
260                mRestoredWithoutUids.put(pkg, r);
261            } else {
262                mRecords.put(key, r);
263            }
264            clampDefaultChannel(r);
265        }
266        return r;
267    }
268
269    // Clamp the importance level of the default channel for apps targeting the new SDK version,
270    // unless the user has already changed the importance.
271    private void clampDefaultChannel(Record r) {
272        try {
273            if (r.uid != Record.UNKNOWN_UID) {
274                int userId = UserHandle.getUserId(r.uid);
275                final ApplicationInfo applicationInfo =
276                        mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
277                if (applicationInfo.targetSdkVersion > Build.VERSION_CODES.N_MR1) {
278                    final NotificationChannel defaultChannel =
279                            r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
280                    if ((defaultChannel.getUserLockedFields()
281                            & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
282                        defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW);
283                        updateConfig();
284                    }
285                }
286            }
287        } catch (NameNotFoundException e) {
288            // oh well.
289        }
290    }
291
292    private void createDefaultChannelIfMissing(Record r) {
293        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
294            NotificationChannel channel;
295            channel = new NotificationChannel(
296                    NotificationChannel.DEFAULT_CHANNEL_ID,
297                    R.string.default_notification_channel_label,
298                    r.importance);
299            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
300            channel.setLockscreenVisibility(r.visibility);
301            if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
302                channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
303            }
304            if (r.priority != DEFAULT_PRIORITY) {
305                channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
306            }
307            if (r.visibility != DEFAULT_VISIBILITY) {
308                channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
309            }
310            r.channels.put(channel.getId(), channel);
311        }
312    }
313
314    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
315        out.startTag(null, TAG_RANKING);
316        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
317
318        final int N = mRecords.size();
319        for (int i = 0; i < N; i++) {
320            final Record r = mRecords.valueAt(i);
321            //TODO: http://b/22388012
322            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
323                continue;
324            }
325            final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
326                    || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
327                    || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
328                    || r.groups.size() > 0;
329            if (hasNonDefaultSettings) {
330                out.startTag(null, TAG_PACKAGE);
331                out.attribute(null, ATT_NAME, r.pkg);
332                if (r.importance != DEFAULT_IMPORTANCE) {
333                    out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
334                }
335                if (r.priority != DEFAULT_PRIORITY) {
336                    out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
337                }
338                if (r.visibility != DEFAULT_VISIBILITY) {
339                    out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
340                }
341                out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
342
343                if (!forBackup) {
344                    out.attribute(null, ATT_UID, Integer.toString(r.uid));
345                }
346
347                for (NotificationChannelGroup group : r.groups.values()) {
348                    group.writeXml(out);
349                }
350
351                for (NotificationChannel channel : r.channels.values()) {
352                    channel.writeXml(out);
353                }
354
355                out.endTag(null, TAG_PACKAGE);
356            }
357        }
358        out.endTag(null, TAG_RANKING);
359    }
360
361    private void updateConfig() {
362        final int N = mSignalExtractors.length;
363        for (int i = 0; i < N; i++) {
364            mSignalExtractors[i].setConfig(this);
365        }
366        mRankingHandler.requestSort(false);
367    }
368
369    public void sort(ArrayList<NotificationRecord> notificationList) {
370        final int N = notificationList.size();
371        // clear global sort keys
372        for (int i = N - 1; i >= 0; i--) {
373            notificationList.get(i).setGlobalSortKey(null);
374        }
375
376        // rank each record individually
377        Collections.sort(notificationList, mPreliminaryComparator);
378
379        synchronized (mProxyByGroupTmp) {
380            // record individual ranking result and nominate proxies for each group
381            for (int i = N - 1; i >= 0; i--) {
382                final NotificationRecord record = notificationList.get(i);
383                record.setAuthoritativeRank(i);
384                final String groupKey = record.getGroupKey();
385                boolean isGroupSummary = record.getNotification().isGroupSummary();
386                if (isGroupSummary || !mProxyByGroupTmp.containsKey(groupKey)) {
387                    mProxyByGroupTmp.put(groupKey, record);
388                }
389            }
390            // assign global sort key:
391            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
392            for (int i = 0; i < N; i++) {
393                final NotificationRecord record = notificationList.get(i);
394                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
395                String groupSortKey = record.getNotification().getSortKey();
396
397                // We need to make sure the developer provided group sort key (gsk) is handled
398                // correctly:
399                //   gsk="" < gsk=non-null-string < gsk=null
400                //
401                // We enforce this by using different prefixes for these three cases.
402                String groupSortKeyPortion;
403                if (groupSortKey == null) {
404                    groupSortKeyPortion = "nsk";
405                } else if (groupSortKey.equals("")) {
406                    groupSortKeyPortion = "esk";
407                } else {
408                    groupSortKeyPortion = "gsk=" + groupSortKey;
409                }
410
411                boolean isGroupSummary = record.getNotification().isGroupSummary();
412                record.setGlobalSortKey(
413                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
414                        record.isRecentlyIntrusive() ? '0' : '1',
415                        groupProxy.getAuthoritativeRank(),
416                        isGroupSummary ? '0' : '1',
417                        groupSortKeyPortion,
418                        record.getAuthoritativeRank()));
419            }
420            mProxyByGroupTmp.clear();
421        }
422
423        // Do a second ranking pass, using group proxies
424        Collections.sort(notificationList, mFinalComparator);
425    }
426
427    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
428        return Collections.binarySearch(notificationList, target, mFinalComparator);
429    }
430
431    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
432        final String value = parser.getAttributeValue(null, att);
433        if (TextUtils.isEmpty(value)) return defValue;
434        return Boolean.parseBoolean(value);
435    }
436
437    private static int safeInt(XmlPullParser parser, String att, int defValue) {
438        final String val = parser.getAttributeValue(null, att);
439        return tryParseInt(val, defValue);
440    }
441
442    private static int tryParseInt(String value, int defValue) {
443        if (TextUtils.isEmpty(value)) return defValue;
444        try {
445            return Integer.parseInt(value);
446        } catch (NumberFormatException e) {
447            return defValue;
448        }
449    }
450
451    /**
452     * Gets importance.
453     */
454    @Override
455    public int getImportance(String packageName, int uid) {
456        return getOrCreateRecord(packageName, uid).importance;
457    }
458
459    @Override
460    public boolean canShowBadge(String packageName, int uid) {
461        return getOrCreateRecord(packageName, uid).showBadge;
462    }
463
464    @Override
465    public void setShowBadge(String packageName, int uid, boolean showBadge) {
466        getOrCreateRecord(packageName, uid).showBadge = showBadge;
467        updateConfig();
468    }
469
470    @Override
471    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
472            boolean fromTargetApp) {
473        Preconditions.checkNotNull(pkg);
474        Preconditions.checkNotNull(group);
475        Preconditions.checkNotNull(group.getId());
476        Preconditions.checkNotNull(group.getName());
477        Record r = getOrCreateRecord(pkg, uid);
478        if (r == null) {
479            throw new IllegalArgumentException("Invalid package");
480        }
481        r.groups.put(group.getId(), group);
482        updateConfig();
483    }
484
485    @Override
486    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
487            boolean fromTargetApp) {
488        Preconditions.checkNotNull(pkg);
489        Preconditions.checkNotNull(channel);
490        Preconditions.checkNotNull(channel.getId());
491        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())
492                || channel.getNameResId() != 0);
493        Record r = getOrCreateRecord(pkg, uid);
494        if (r == null) {
495            throw new IllegalArgumentException("Invalid package");
496        }
497        if (IMPORTANCE_NONE == r.importance) {
498            throw new IllegalArgumentException("Package blocked");
499        }
500        if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
501            throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
502        }
503
504        NotificationChannel existing = r.channels.get(channel.getId());
505        // Keep existing settings
506        if (existing != null) {
507            if (existing.isDeleted()) {
508                existing.setDeleted(false);
509                updateConfig();
510            }
511            return;
512        }
513        if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
514                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
515            throw new IllegalArgumentException("Invalid importance level");
516        }
517        // Reset fields that apps aren't allowed to set.
518        if (fromTargetApp) {
519            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
520            channel.setLockscreenVisibility(r.visibility);
521        }
522        clearLockedFields(channel);
523        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
524            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
525        }
526        if (!r.showBadge) {
527            channel.setShowBadge(false);
528        }
529        if (channel.getSound() == null) {
530            channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
531                    Notification.AUDIO_ATTRIBUTES_DEFAULT);
532        }
533        r.channels.put(channel.getId(), channel);
534        updateConfig();
535    }
536
537    private void clearLockedFields(NotificationChannel channel) {
538        int clearMask = 0;
539        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
540            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
541        }
542        channel.lockFields(~clearMask);
543    }
544
545    @Override
546    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
547        Preconditions.checkNotNull(updatedChannel);
548        Preconditions.checkNotNull(updatedChannel.getId());
549        Record r = getOrCreateRecord(pkg, uid);
550        if (r == null) {
551            throw new IllegalArgumentException("Invalid package");
552        }
553        NotificationChannel channel = r.channels.get(updatedChannel.getId());
554        if (channel == null || channel.isDeleted()) {
555            throw new IllegalArgumentException("Channel does not exist");
556        }
557        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
558            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
559        }
560        r.channels.put(updatedChannel.getId(), updatedChannel);
561        updateConfig();
562    }
563
564    @Override
565    public void updateNotificationChannelFromAssistant(String pkg, int uid,
566            NotificationChannel updatedChannel) {
567        Record r = getOrCreateRecord(pkg, uid);
568        if (r == null) {
569            throw new IllegalArgumentException("Invalid package");
570        }
571        NotificationChannel channel = r.channels.get(updatedChannel.getId());
572        if (channel == null || channel.isDeleted()) {
573            throw new IllegalArgumentException("Channel does not exist");
574        }
575
576        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
577            channel.setImportance(updatedChannel.getImportance());
578        }
579        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_LIGHTS) == 0) {
580            channel.enableLights(updatedChannel.shouldShowLights());
581            channel.setLightColor(updatedChannel.getLightColor());
582        }
583        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0) {
584            channel.setBypassDnd(updatedChannel.canBypassDnd());
585        }
586        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) {
587            channel.setSound(updatedChannel.getSound(), updatedChannel.getAudioAttributes());
588        }
589        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
590            channel.enableVibration(updatedChannel.shouldVibrate());
591            channel.setVibrationPattern(updatedChannel.getVibrationPattern());
592        }
593        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) {
594            if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
595                channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
596            } else {
597                channel.setLockscreenVisibility(updatedChannel.getLockscreenVisibility());
598            }
599        }
600        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0) {
601            channel.setShowBadge(updatedChannel.canShowBadge());
602        }
603        if (updatedChannel.isDeleted()) {
604            channel.setDeleted(true);
605        }
606        // Assistant cannot change the group
607
608        r.channels.put(channel.getId(), channel);
609        updateConfig();
610    }
611
612    @Override
613    public NotificationChannel getNotificationChannelWithFallback(String pkg, int uid,
614            String channelId, boolean includeDeleted) {
615        Record r = getOrCreateRecord(pkg, uid);
616        if (channelId == null) {
617            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
618        }
619        NotificationChannel channel = r.channels.get(channelId);
620        if (channel != null && (includeDeleted || !channel.isDeleted())) {
621            return channel;
622        } else {
623            return r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
624        }
625    }
626
627    @Override
628    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
629            boolean includeDeleted) {
630        Preconditions.checkNotNull(pkg);
631        Record r = getOrCreateRecord(pkg, uid);
632        if (r == null) {
633            return null;
634        }
635        if (channelId == null) {
636            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
637        }
638        final NotificationChannel nc = r.channels.get(channelId);
639        if (nc != null && (includeDeleted || !nc.isDeleted())) {
640            return nc;
641        }
642        return null;
643    }
644
645    @Override
646    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
647        Preconditions.checkNotNull(pkg);
648        Preconditions.checkNotNull(channelId);
649        Record r = getRecord(pkg, uid);
650        if (r == null) {
651            return;
652        }
653        NotificationChannel channel = r.channels.get(channelId);
654        if (channel != null) {
655            channel.setDeleted(true);
656        }
657    }
658
659    @Override
660    @VisibleForTesting
661    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
662        Preconditions.checkNotNull(pkg);
663        Preconditions.checkNotNull(channelId);
664        Record r = getRecord(pkg, uid);
665        if (r == null) {
666            return;
667        }
668        r.channels.remove(channelId);
669    }
670
671    @Override
672    public void permanentlyDeleteNotificationChannels(String pkg, int uid) {
673        Preconditions.checkNotNull(pkg);
674        Record r = getRecord(pkg, uid);
675        if (r == null) {
676            return;
677        }
678        int N = r.channels.size() - 1;
679        for (int i = N; i >= 0; i--) {
680            String key = r.channels.keyAt(i);
681            if (!NotificationChannel.DEFAULT_CHANNEL_ID.equals(key)) {
682                r.channels.remove(key);
683            }
684        }
685    }
686
687    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
688            int uid) {
689        Preconditions.checkNotNull(pkg);
690        Record r = getRecord(pkg, uid);
691        return r.groups.get(groupId);
692    }
693
694    @Override
695    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
696            int uid, boolean includeDeleted) {
697        Preconditions.checkNotNull(pkg);
698        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
699        Record r = getRecord(pkg, uid);
700        if (r == null) {
701            return ParceledListSlice.emptyList();
702        }
703        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
704        int N = r.channels.size();
705        for (int i = 0; i < N; i++) {
706            final NotificationChannel nc = r.channels.valueAt(i);
707            if (includeDeleted || !nc.isDeleted()) {
708                if (nc.getGroup() != null) {
709                    NotificationChannelGroup ncg = groups.get(nc.getGroup());
710                    if (ncg == null ) {
711                        ncg = r.groups.get(nc.getGroup()).clone();
712                        groups.put(nc.getGroup(), ncg);
713                    }
714                    ncg.addChannel(nc);
715                } else {
716                    nonGrouped.addChannel(nc);
717                }
718            }
719        }
720        if (nonGrouped.getChannels().size() > 0) {
721            groups.put(null, nonGrouped);
722        }
723        return new ParceledListSlice<>(new ArrayList<>(groups.values()));
724    }
725
726    @Override
727    @VisibleForTesting
728    public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
729            int uid) {
730        Record r = getRecord(pkg, uid);
731        if (r == null) {
732            return new ArrayList<>();
733        }
734        return r.groups.values();
735    }
736
737    @Override
738    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
739            boolean includeDeleted) {
740        Preconditions.checkNotNull(pkg);
741        List<NotificationChannel> channels = new ArrayList<>();
742        Record r = getRecord(pkg, uid);
743        if (r == null) {
744            return ParceledListSlice.emptyList();
745        }
746        int N = r.channels.size();
747        for (int i = 0; i < N; i++) {
748            final NotificationChannel nc = r.channels.valueAt(i);
749            if (includeDeleted || !nc.isDeleted()) {
750                channels.add(nc);
751            }
752        }
753        return new ParceledListSlice<>(channels);
754    }
755
756    /**
757     * Sets importance.
758     */
759    @Override
760    public void setImportance(String pkgName, int uid, int importance) {
761        getOrCreateRecord(pkgName, uid).importance = importance;
762        updateConfig();
763    }
764
765    public void setEnabled(String packageName, int uid, boolean enabled) {
766        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
767        if (wasEnabled == enabled) {
768            return;
769        }
770        setImportance(packageName, uid,
771                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
772    }
773
774    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
775        if (filter == null) {
776            final int N = mSignalExtractors.length;
777            pw.print(prefix);
778            pw.print("mSignalExtractors.length = ");
779            pw.println(N);
780            for (int i = 0; i < N; i++) {
781                pw.print(prefix);
782                pw.print("  ");
783                pw.println(mSignalExtractors[i]);
784            }
785        }
786        if (filter == null) {
787            pw.print(prefix);
788            pw.println("per-package config:");
789        }
790        pw.println("Records:");
791        dumpRecords(pw, prefix, filter, mRecords);
792        pw.println("Restored without uid:");
793        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
794    }
795
796    private static void dumpRecords(PrintWriter pw, String prefix,
797            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
798        final int N = records.size();
799        for (int i = 0; i < N; i++) {
800            final Record r = records.valueAt(i);
801            if (filter == null || filter.matches(r.pkg)) {
802                pw.print(prefix);
803                pw.print("  AppSettings: ");
804                pw.print(r.pkg);
805                pw.print(" (");
806                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
807                pw.print(')');
808                if (r.importance != DEFAULT_IMPORTANCE) {
809                    pw.print(" importance=");
810                    pw.print(Ranking.importanceToString(r.importance));
811                }
812                if (r.priority != DEFAULT_PRIORITY) {
813                    pw.print(" priority=");
814                    pw.print(Notification.priorityToString(r.priority));
815                }
816                if (r.visibility != DEFAULT_VISIBILITY) {
817                    pw.print(" visibility=");
818                    pw.print(Notification.visibilityToString(r.visibility));
819                }
820                pw.print(" showBadge=");
821                pw.print(Boolean.toString(r.showBadge));
822                pw.println();
823                for (NotificationChannel channel : r.channels.values()) {
824                    pw.print(prefix);
825                    pw.print("  ");
826                    pw.print("  ");
827                    pw.println(channel);
828                }
829                for (NotificationChannelGroup group : r.groups.values()) {
830                    pw.print(prefix);
831                    pw.print("  ");
832                    pw.print("  ");
833                    pw.println(group);
834                }
835            }
836        }
837    }
838
839    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
840        JSONObject ranking = new JSONObject();
841        JSONArray records = new JSONArray();
842        try {
843            ranking.put("noUid", mRestoredWithoutUids.size());
844        } catch (JSONException e) {
845           // pass
846        }
847        final int N = mRecords.size();
848        for (int i = 0; i < N; i++) {
849            final Record r = mRecords.valueAt(i);
850            if (filter == null || filter.matches(r.pkg)) {
851                JSONObject record = new JSONObject();
852                try {
853                    record.put("userId", UserHandle.getUserId(r.uid));
854                    record.put("packageName", r.pkg);
855                    if (r.importance != DEFAULT_IMPORTANCE) {
856                        record.put("importance", Ranking.importanceToString(r.importance));
857                    }
858                    if (r.priority != DEFAULT_PRIORITY) {
859                        record.put("priority", Notification.priorityToString(r.priority));
860                    }
861                    if (r.visibility != DEFAULT_VISIBILITY) {
862                        record.put("visibility", Notification.visibilityToString(r.visibility));
863                    }
864                    if (r.showBadge != DEFAULT_SHOW_BADGE) {
865                        record.put("showBadge", Boolean.valueOf(r.showBadge));
866                    }
867                    for (NotificationChannel channel : r.channels.values()) {
868                        record.put("channel", channel.toJson());
869                    }
870                    for (NotificationChannelGroup group : r.groups.values()) {
871                        record.put("group", group.toJson());
872                    }
873                } catch (JSONException e) {
874                   // pass
875                }
876                records.put(record);
877            }
878        }
879        try {
880            ranking.put("records", records);
881        } catch (JSONException e) {
882            // pass
883        }
884        return ranking;
885    }
886
887    /**
888     * Dump only the ban information as structured JSON for the stats collector.
889     *
890     * This is intentionally redundant with {#link dumpJson} because the old
891     * scraper will expect this format.
892     *
893     * @param filter
894     * @return
895     */
896    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
897        JSONArray bans = new JSONArray();
898        Map<Integer, String> packageBans = getPackageBans();
899        for(Entry<Integer, String> ban : packageBans.entrySet()) {
900            final int userId = UserHandle.getUserId(ban.getKey());
901            final String packageName = ban.getValue();
902            if (filter == null || filter.matches(packageName)) {
903                JSONObject banJson = new JSONObject();
904                try {
905                    banJson.put("userId", userId);
906                    banJson.put("packageName", packageName);
907                } catch (JSONException e) {
908                    e.printStackTrace();
909                }
910                bans.put(banJson);
911            }
912        }
913        return bans;
914    }
915
916    public Map<Integer, String> getPackageBans() {
917        final int N = mRecords.size();
918        ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
919        for (int i = 0; i < N; i++) {
920            final Record r = mRecords.valueAt(i);
921            if (r.importance == NotificationManager.IMPORTANCE_NONE) {
922                packageBans.put(r.uid, r.pkg);
923            }
924        }
925        return packageBans;
926    }
927
928    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
929            int[] uidList) {
930        if (pkgList == null || pkgList.length == 0) {
931            return; // nothing to do
932        }
933        boolean updated = false;
934        if (removingPackage) {
935            // Remove notification settings for uninstalled package
936            int size = Math.min(pkgList.length, uidList.length);
937            for (int i = 0; i < size; i++) {
938                final String pkg = pkgList[i];
939                final int uid = uidList[i];
940                mRecords.remove(recordKey(pkg, uid));
941                mRestoredWithoutUids.remove(pkg);
942                updated = true;
943            }
944        } else {
945            for (String pkg : pkgList) {
946                // Package install
947                final Record r = mRestoredWithoutUids.get(pkg);
948                if (r != null) {
949                    try {
950                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
951                        mRestoredWithoutUids.remove(pkg);
952                        mRecords.put(recordKey(r.pkg, r.uid), r);
953                        updated = true;
954                    } catch (NameNotFoundException e) {
955                        // noop
956                    }
957                }
958                // Package upgrade
959                try {
960                    Record fullRecord = getRecord(pkg,
961                            mPm.getPackageUidAsUser(pkg, changeUserId));
962                    if (fullRecord != null) {
963                        clampDefaultChannel(fullRecord);
964                    }
965                } catch (NameNotFoundException e) {
966                }
967            }
968        }
969
970        if (updated) {
971            updateConfig();
972        }
973    }
974
975    private static class Record {
976        static int UNKNOWN_UID = UserHandle.USER_NULL;
977
978        String pkg;
979        int uid = UNKNOWN_UID;
980        int importance = DEFAULT_IMPORTANCE;
981        int priority = DEFAULT_PRIORITY;
982        int visibility = DEFAULT_VISIBILITY;
983        boolean showBadge = DEFAULT_SHOW_BADGE;
984
985        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
986        ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>();
987   }
988}
989