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