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