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