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