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