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