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