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