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