RankingHelper.java revision 03fa85ddccb30a6f1dcfeaec9dcbba062e5f8573
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 static android.app.NotificationManager.IMPORTANCE_NONE;
19
20import com.android.internal.R;
21import com.android.internal.annotations.VisibleForTesting;
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.os.Build;
34import android.os.UserHandle;
35import android.provider.Settings;
36import android.service.notification.NotificationListenerService.Ranking;
37import android.text.TextUtils;
38import android.util.ArrayMap;
39import android.util.Slog;
40
41import org.json.JSONArray;
42import org.json.JSONException;
43import org.json.JSONObject;
44import org.xmlpull.v1.XmlPullParser;
45import org.xmlpull.v1.XmlPullParserException;
46import org.xmlpull.v1.XmlSerializer;
47
48import java.io.IOException;
49import java.io.PrintWriter;
50import java.util.ArrayList;
51import java.util.Collection;
52import java.util.Collections;
53import java.util.List;
54import java.util.Map;
55import java.util.Map.Entry;
56
57public class RankingHelper implements RankingConfig {
58    private static final String TAG = "RankingHelper";
59
60    private static final int XML_VERSION = 1;
61
62    private static final String TAG_RANKING = "ranking";
63    private static final String TAG_PACKAGE = "package";
64    private static final String TAG_CHANNEL = "channel";
65    private static final String TAG_GROUP = "channelGroup";
66
67    private static final String ATT_VERSION = "version";
68    private static final String ATT_NAME = "name";
69    private static final String ATT_NAME_RES_ID = "name_res_id";
70    private static final String ATT_UID = "uid";
71    private static final String ATT_ID = "id";
72    private static final String ATT_PRIORITY = "priority";
73    private static final String ATT_VISIBILITY = "visibility";
74    private static final String ATT_IMPORTANCE = "importance";
75    private static final String ATT_SHOW_BADGE = "show_badge";
76
77    private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
78    private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
79    private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
80    private static final boolean DEFAULT_SHOW_BADGE = true;
81
82    private final NotificationSignalExtractor[] mSignalExtractors;
83    private final NotificationComparator mPreliminaryComparator;
84    private final GlobalSortKeyComparator mFinalComparator = new GlobalSortKeyComparator();
85
86    private final ArrayMap<String, Record> mRecords = new ArrayMap<>(); // pkg|uid => Record
87    private final ArrayMap<String, NotificationRecord> mProxyByGroupTmp = new ArrayMap<>();
88    private final ArrayMap<String, Record> mRestoredWithoutUids = new ArrayMap<>(); // pkg => Record
89
90    private final Context mContext;
91    private final RankingHandler mRankingHandler;
92    private final PackageManager mPm;
93
94    public RankingHelper(Context context, PackageManager pm, RankingHandler rankingHandler,
95            NotificationUsageStats usageStats, String[] extractorNames) {
96        mContext = context;
97        mRankingHandler = rankingHandler;
98        mPm = pm;
99
100        mPreliminaryComparator = new NotificationComparator(mContext);
101
102        final int N = extractorNames.length;
103        mSignalExtractors = new NotificationSignalExtractor[N];
104        for (int i = 0; i < N; i++) {
105            try {
106                Class<?> extractorClass = mContext.getClassLoader().loadClass(extractorNames[i]);
107                NotificationSignalExtractor extractor =
108                        (NotificationSignalExtractor) extractorClass.newInstance();
109                extractor.initialize(mContext, usageStats);
110                extractor.setConfig(this);
111                mSignalExtractors[i] = extractor;
112            } catch (ClassNotFoundException e) {
113                Slog.w(TAG, "Couldn't find extractor " + extractorNames[i] + ".", e);
114            } catch (InstantiationException e) {
115                Slog.w(TAG, "Couldn't instantiate extractor " + extractorNames[i] + ".", e);
116            } catch (IllegalAccessException e) {
117                Slog.w(TAG, "Problem accessing extractor " + extractorNames[i] + ".", e);
118            }
119        }
120    }
121
122    @SuppressWarnings("unchecked")
123    public <T extends NotificationSignalExtractor> T findExtractor(Class<T> extractorClass) {
124        final int N = mSignalExtractors.length;
125        for (int i = 0; i < N; i++) {
126            final NotificationSignalExtractor extractor = mSignalExtractors[i];
127            if (extractorClass.equals(extractor.getClass())) {
128                return (T) extractor;
129            }
130        }
131        return null;
132    }
133
134    public void extractSignals(NotificationRecord r) {
135        final int N = mSignalExtractors.length;
136        for (int i = 0; i < N; i++) {
137            NotificationSignalExtractor extractor = mSignalExtractors[i];
138            try {
139                RankingReconsideration recon = extractor.process(r);
140                if (recon != null) {
141                    mRankingHandler.requestReconsideration(recon);
142                }
143            } catch (Throwable t) {
144                Slog.w(TAG, "NotificationSignalExtractor failed.", t);
145            }
146        }
147    }
148
149    public void readXml(XmlPullParser parser, boolean forRestore)
150            throws XmlPullParserException, IOException {
151        int type = parser.getEventType();
152        if (type != XmlPullParser.START_TAG) return;
153        String tag = parser.getName();
154        if (!TAG_RANKING.equals(tag)) return;
155        mRecords.clear();
156        mRestoredWithoutUids.clear();
157        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
158            tag = parser.getName();
159            if (type == XmlPullParser.END_TAG && TAG_RANKING.equals(tag)) {
160                return;
161            }
162            if (type == XmlPullParser.START_TAG) {
163                if (TAG_PACKAGE.equals(tag)) {
164                    int uid = safeInt(parser, ATT_UID, Record.UNKNOWN_UID);
165                    String name = parser.getAttributeValue(null, ATT_NAME);
166                    if (!TextUtils.isEmpty(name)) {
167                        if (forRestore) {
168                            try {
169                                //TODO: http://b/22388012
170                                uid = mPm.getPackageUidAsUser(name, UserHandle.USER_SYSTEM);
171                            } catch (NameNotFoundException e) {
172                                // noop
173                            }
174                        }
175
176                        Record r = getOrCreateRecord(name, uid,
177                                safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE),
178                                safeInt(parser, ATT_PRIORITY, DEFAULT_PRIORITY),
179                                safeInt(parser, ATT_VISIBILITY, DEFAULT_VISIBILITY),
180                                safeBool(parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE));
181
182                        final int innerDepth = parser.getDepth();
183                        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
184                                && (type != XmlPullParser.END_TAG
185                                || parser.getDepth() > innerDepth)) {
186                            if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
187                                continue;
188                            }
189
190                            String tagName = parser.getName();
191                            // Channel groups
192                            if (TAG_GROUP.equals(tagName)) {
193                                String id = parser.getAttributeValue(null, ATT_ID);
194                                CharSequence groupName = parser.getAttributeValue(null, ATT_NAME);
195                                int groupNameRes = safeInt(parser, ATT_NAME_RES_ID, 0);
196                                if (!TextUtils.isEmpty(id)) {
197                                    NotificationChannelGroup group = null;
198                                    if (groupName != null) {
199                                        group = new NotificationChannelGroup(id, groupName);
200                                    } else {
201                                        group = new NotificationChannelGroup(id, groupNameRes);
202                                    }
203                                    r.groups.put(id, group);
204                                }
205                            }
206                            // Channels
207                            if (TAG_CHANNEL.equals(tagName)) {
208                                String id = parser.getAttributeValue(null, ATT_ID);
209                                CharSequence channelName = parser.getAttributeValue(null, ATT_NAME);
210                                int channelNameRes = safeInt(parser, ATT_NAME_RES_ID, 0);
211                                int channelImportance =
212                                        safeInt(parser, ATT_IMPORTANCE, DEFAULT_IMPORTANCE);
213
214                                if (!TextUtils.isEmpty(id)) {
215                                    NotificationChannel channel;
216                                    if (channelName != null) {
217                                        channel = new NotificationChannel(id, channelName,
218                                                channelImportance);
219                                    } else {
220                                        channel = new NotificationChannel(id, channelNameRes,
221                                                channelImportance);
222                                    }
223                                    channel.populateFromXml(parser);
224                                    r.channels.put(id, channel);
225                                }
226                            }
227                        }
228
229                        clampDefaultChannel(r);
230                    }
231                }
232            }
233        }
234        throw new IllegalStateException("Failed to reach END_DOCUMENT");
235    }
236
237    private static String recordKey(String pkg, int uid) {
238        return pkg + "|" + uid;
239    }
240
241    private Record getRecord(String pkg, int uid) {
242        final String key = recordKey(pkg, uid);
243        return mRecords.get(key);
244    }
245
246    private Record getOrCreateRecord(String pkg, int uid) {
247        return getOrCreateRecord(pkg, uid,
248                DEFAULT_IMPORTANCE, DEFAULT_PRIORITY, DEFAULT_VISIBILITY, DEFAULT_SHOW_BADGE);
249    }
250
251    private Record getOrCreateRecord(String pkg, int uid, int importance, int priority,
252            int visibility, boolean showBadge) {
253        final String key = recordKey(pkg, uid);
254        Record r = (uid == Record.UNKNOWN_UID) ? mRestoredWithoutUids.get(pkg) : mRecords.get(key);
255        if (r == null) {
256            r = new Record();
257            r.pkg = pkg;
258            r.uid = uid;
259            r.importance = importance;
260            r.priority = priority;
261            r.visibility = visibility;
262            r.showBadge = showBadge;
263            createDefaultChannelIfMissing(r);
264            if (r.uid == Record.UNKNOWN_UID) {
265                mRestoredWithoutUids.put(pkg, r);
266            } else {
267                mRecords.put(key, r);
268            }
269            clampDefaultChannel(r);
270        }
271        return r;
272    }
273
274    // Clamp the importance level of the default channel for apps targeting the new SDK version,
275    // unless the user has already changed the importance.
276    private void clampDefaultChannel(Record r) {
277        try {
278            if (r.uid != Record.UNKNOWN_UID) {
279                int userId = UserHandle.getUserId(r.uid);
280                final ApplicationInfo applicationInfo =
281                        mPm.getApplicationInfoAsUser(r.pkg, 0, userId);
282                if (applicationInfo.targetSdkVersion > Build.VERSION_CODES.N_MR1) {
283                    final NotificationChannel defaultChannel =
284                            r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
285                    if ((defaultChannel.getUserLockedFields()
286                            & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
287                        defaultChannel.setImportance(NotificationManager.IMPORTANCE_LOW);
288                        updateConfig();
289                    }
290                }
291            }
292        } catch (NameNotFoundException e) {
293            // oh well.
294        }
295    }
296
297    private void createDefaultChannelIfMissing(Record r) {
298        if (!r.channels.containsKey(NotificationChannel.DEFAULT_CHANNEL_ID)) {
299            NotificationChannel channel;
300            channel = new NotificationChannel(
301                    NotificationChannel.DEFAULT_CHANNEL_ID,
302                    R.string.default_notification_channel_label,
303                    r.importance);
304            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
305            channel.setLockscreenVisibility(r.visibility);
306            if (r.importance != NotificationManager.IMPORTANCE_UNSPECIFIED) {
307                channel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
308            }
309            if (r.priority != DEFAULT_PRIORITY) {
310                channel.lockFields(NotificationChannel.USER_LOCKED_PRIORITY);
311            }
312            if (r.visibility != DEFAULT_VISIBILITY) {
313                channel.lockFields(NotificationChannel.USER_LOCKED_VISIBILITY);
314            }
315            r.channels.put(channel.getId(), channel);
316        }
317    }
318
319    public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
320        out.startTag(null, TAG_RANKING);
321        out.attribute(null, ATT_VERSION, Integer.toString(XML_VERSION));
322
323        final int N = mRecords.size();
324        for (int i = 0; i < N; i++) {
325            final Record r = mRecords.valueAt(i);
326            //TODO: http://b/22388012
327            if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
328                continue;
329            }
330            final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
331                    || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
332                    || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
333                    || r.groups.size() > 0;
334            if (hasNonDefaultSettings) {
335                out.startTag(null, TAG_PACKAGE);
336                out.attribute(null, ATT_NAME, r.pkg);
337                if (r.importance != DEFAULT_IMPORTANCE) {
338                    out.attribute(null, ATT_IMPORTANCE, Integer.toString(r.importance));
339                }
340                if (r.priority != DEFAULT_PRIORITY) {
341                    out.attribute(null, ATT_PRIORITY, Integer.toString(r.priority));
342                }
343                if (r.visibility != DEFAULT_VISIBILITY) {
344                    out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
345                }
346                out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
347
348                if (!forBackup) {
349                    out.attribute(null, ATT_UID, Integer.toString(r.uid));
350                }
351
352                for (NotificationChannelGroup group : r.groups.values()) {
353                    group.writeXml(out);
354                }
355
356                for (NotificationChannel channel : r.channels.values()) {
357                    channel.writeXml(out);
358                }
359
360                out.endTag(null, TAG_PACKAGE);
361            }
362        }
363        out.endTag(null, TAG_RANKING);
364    }
365
366    private void updateConfig() {
367        final int N = mSignalExtractors.length;
368        for (int i = 0; i < N; i++) {
369            mSignalExtractors[i].setConfig(this);
370        }
371        mRankingHandler.requestSort(false);
372    }
373
374    public void sort(ArrayList<NotificationRecord> notificationList) {
375        final int N = notificationList.size();
376        // clear global sort keys
377        for (int i = N - 1; i >= 0; i--) {
378            notificationList.get(i).setGlobalSortKey(null);
379        }
380
381        // rank each record individually
382        Collections.sort(notificationList, mPreliminaryComparator);
383
384        synchronized (mProxyByGroupTmp) {
385            // record individual ranking result and nominate proxies for each group
386            for (int i = N - 1; i >= 0; i--) {
387                final NotificationRecord record = notificationList.get(i);
388                record.setAuthoritativeRank(i);
389                final String groupKey = record.getGroupKey();
390                NotificationRecord existingProxy = mProxyByGroupTmp.get(groupKey);
391                if (existingProxy == null
392                        || record.getImportance() > existingProxy.getImportance()) {
393                    mProxyByGroupTmp.put(groupKey, record);
394                }
395            }
396            // assign global sort key:
397            //   is_recently_intrusive:group_rank:is_group_summary:group_sort_key:rank
398            for (int i = 0; i < N; i++) {
399                final NotificationRecord record = notificationList.get(i);
400                NotificationRecord groupProxy = mProxyByGroupTmp.get(record.getGroupKey());
401                String groupSortKey = record.getNotification().getSortKey();
402
403                // We need to make sure the developer provided group sort key (gsk) is handled
404                // correctly:
405                //   gsk="" < gsk=non-null-string < gsk=null
406                //
407                // We enforce this by using different prefixes for these three cases.
408                String groupSortKeyPortion;
409                if (groupSortKey == null) {
410                    groupSortKeyPortion = "nsk";
411                } else if (groupSortKey.equals("")) {
412                    groupSortKeyPortion = "esk";
413                } else {
414                    groupSortKeyPortion = "gsk=" + groupSortKey;
415                }
416
417                boolean isGroupSummary = record.getNotification().isGroupSummary();
418                record.setGlobalSortKey(
419                        String.format("intrsv=%c:grnk=0x%04x:gsmry=%c:%s:rnk=0x%04x",
420                        record.isRecentlyIntrusive() ? '0' : '1',
421                        groupProxy.getAuthoritativeRank(),
422                        isGroupSummary ? '0' : '1',
423                        groupSortKeyPortion,
424                        record.getAuthoritativeRank()));
425            }
426            mProxyByGroupTmp.clear();
427        }
428
429        // Do a second ranking pass, using group proxies
430        Collections.sort(notificationList, mFinalComparator);
431    }
432
433    public int indexOf(ArrayList<NotificationRecord> notificationList, NotificationRecord target) {
434        return Collections.binarySearch(notificationList, target, mFinalComparator);
435    }
436
437    private static boolean safeBool(XmlPullParser parser, String att, boolean defValue) {
438        final String value = parser.getAttributeValue(null, att);
439        if (TextUtils.isEmpty(value)) return defValue;
440        return Boolean.parseBoolean(value);
441    }
442
443    private static int safeInt(XmlPullParser parser, String att, int defValue) {
444        final String val = parser.getAttributeValue(null, att);
445        return tryParseInt(val, defValue);
446    }
447
448    private static int tryParseInt(String value, int defValue) {
449        if (TextUtils.isEmpty(value)) return defValue;
450        try {
451            return Integer.parseInt(value);
452        } catch (NumberFormatException e) {
453            return defValue;
454        }
455    }
456
457    /**
458     * Gets importance.
459     */
460    @Override
461    public int getImportance(String packageName, int uid) {
462        return getOrCreateRecord(packageName, uid).importance;
463    }
464
465    @Override
466    public boolean canShowBadge(String packageName, int uid) {
467        return getOrCreateRecord(packageName, uid).showBadge;
468    }
469
470    @Override
471    public void setShowBadge(String packageName, int uid, boolean showBadge) {
472        getOrCreateRecord(packageName, uid).showBadge = showBadge;
473        updateConfig();
474    }
475
476    @Override
477    public void createNotificationChannelGroup(String pkg, int uid, NotificationChannelGroup group,
478            boolean fromTargetApp) {
479        Preconditions.checkNotNull(pkg);
480        Preconditions.checkNotNull(group);
481        Preconditions.checkNotNull(group.getId());
482        Preconditions.checkNotNull(!TextUtils.isEmpty(group.getName())
483                || group.getNameResId() != 0);
484        Record r = getOrCreateRecord(pkg, uid);
485        if (r == null) {
486            throw new IllegalArgumentException("Invalid package");
487        }
488        r.groups.put(group.getId(), group);
489        updateConfig();
490    }
491
492    @Override
493    public void createNotificationChannel(String pkg, int uid, NotificationChannel channel,
494            boolean fromTargetApp) {
495        Preconditions.checkNotNull(pkg);
496        Preconditions.checkNotNull(channel);
497        Preconditions.checkNotNull(channel.getId());
498        Preconditions.checkArgument(!TextUtils.isEmpty(channel.getName())
499                || channel.getNameResId() != 0);
500        Record r = getOrCreateRecord(pkg, uid);
501        if (r == null) {
502            throw new IllegalArgumentException("Invalid package");
503        }
504        if (IMPORTANCE_NONE == r.importance) {
505            throw new IllegalArgumentException("Package blocked");
506        }
507        if (channel.getGroup() != null && !r.groups.containsKey(channel.getGroup())) {
508            throw new IllegalArgumentException("NotificationChannelGroup doesn't exist");
509        }
510        if (NotificationChannel.DEFAULT_CHANNEL_ID.equals(channel.getId())) {
511            throw new IllegalArgumentException("Reserved id");
512        }
513
514        NotificationChannel existing = r.channels.get(channel.getId());
515        // Keep existing settings
516        if (existing != null) {
517            if (existing.isDeleted()) {
518                existing.setDeleted(false);
519                updateConfig();
520            }
521            return;
522        }
523        if (channel.getImportance() < NotificationManager.IMPORTANCE_NONE
524                || channel.getImportance() > NotificationManager.IMPORTANCE_MAX) {
525            throw new IllegalArgumentException("Invalid importance level");
526        }
527        // Reset fields that apps aren't allowed to set.
528        if (fromTargetApp) {
529            channel.setBypassDnd(r.priority == Notification.PRIORITY_MAX);
530            channel.setLockscreenVisibility(r.visibility);
531        }
532        clearLockedFields(channel);
533        if (channel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
534            channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
535        }
536        if (!r.showBadge) {
537            channel.setShowBadge(false);
538        }
539        if (channel.getSound() == null) {
540            channel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI,
541                    Notification.AUDIO_ATTRIBUTES_DEFAULT);
542        }
543        r.channels.put(channel.getId(), channel);
544        updateConfig();
545    }
546
547    private void clearLockedFields(NotificationChannel channel) {
548        int clearMask = 0;
549        for (int i = 0; i < NotificationChannel.LOCKABLE_FIELDS.length; i++) {
550            clearMask |= NotificationChannel.LOCKABLE_FIELDS[i];
551        }
552        channel.lockFields(~clearMask);
553    }
554
555    @Override
556    public void updateNotificationChannel(String pkg, int uid, NotificationChannel updatedChannel) {
557        Preconditions.checkNotNull(updatedChannel);
558        Preconditions.checkNotNull(updatedChannel.getId());
559        Record r = getOrCreateRecord(pkg, uid);
560        if (r == null) {
561            throw new IllegalArgumentException("Invalid package");
562        }
563        NotificationChannel channel = r.channels.get(updatedChannel.getId());
564        if (channel == null || channel.isDeleted()) {
565            throw new IllegalArgumentException("Channel does not exist");
566        }
567        if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
568            updatedChannel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
569        }
570        r.channels.put(updatedChannel.getId(), updatedChannel);
571        updateConfig();
572    }
573
574    @Override
575    public void updateNotificationChannelFromAssistant(String pkg, int uid,
576            NotificationChannel updatedChannel) {
577        Record r = getOrCreateRecord(pkg, uid);
578        if (r == null) {
579            throw new IllegalArgumentException("Invalid package");
580        }
581        NotificationChannel channel = r.channels.get(updatedChannel.getId());
582        if (channel == null || channel.isDeleted()) {
583            throw new IllegalArgumentException("Channel does not exist");
584        }
585
586        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0) {
587            channel.setImportance(updatedChannel.getImportance());
588        }
589        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_LIGHTS) == 0) {
590            channel.enableLights(updatedChannel.shouldShowLights());
591            channel.setLightColor(updatedChannel.getLightColor());
592        }
593        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_PRIORITY) == 0) {
594            channel.setBypassDnd(updatedChannel.canBypassDnd());
595        }
596        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SOUND) == 0) {
597            channel.setSound(updatedChannel.getSound(), updatedChannel.getAudioAttributes());
598        }
599        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VIBRATION) == 0) {
600            channel.enableVibration(updatedChannel.shouldVibrate());
601            channel.setVibrationPattern(updatedChannel.getVibrationPattern());
602        }
603        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_VISIBILITY) == 0) {
604            if (updatedChannel.getLockscreenVisibility() == Notification.VISIBILITY_PUBLIC) {
605                channel.setLockscreenVisibility(Ranking.VISIBILITY_NO_OVERRIDE);
606            } else {
607                channel.setLockscreenVisibility(updatedChannel.getLockscreenVisibility());
608            }
609        }
610        if ((channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_SHOW_BADGE) == 0) {
611            channel.setShowBadge(updatedChannel.canShowBadge());
612        }
613        if (updatedChannel.isDeleted()) {
614            channel.setDeleted(true);
615        }
616        // Assistant cannot change the group
617
618        r.channels.put(channel.getId(), channel);
619        updateConfig();
620    }
621
622    @Override
623    public NotificationChannel getNotificationChannelWithFallback(String pkg, int uid,
624            String channelId, boolean includeDeleted) {
625        Record r = getOrCreateRecord(pkg, uid);
626        if (channelId == null) {
627            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
628        }
629        NotificationChannel channel = r.channels.get(channelId);
630        if (channel != null && (includeDeleted || !channel.isDeleted())) {
631            return channel;
632        } else {
633            return r.channels.get(NotificationChannel.DEFAULT_CHANNEL_ID);
634        }
635    }
636
637    @Override
638    public NotificationChannel getNotificationChannel(String pkg, int uid, String channelId,
639            boolean includeDeleted) {
640        Preconditions.checkNotNull(pkg);
641        Record r = getOrCreateRecord(pkg, uid);
642        if (r == null) {
643            return null;
644        }
645        if (channelId == null) {
646            channelId = NotificationChannel.DEFAULT_CHANNEL_ID;
647        }
648        final NotificationChannel nc = r.channels.get(channelId);
649        if (nc != null && (includeDeleted || !nc.isDeleted())) {
650            return nc;
651        }
652        return null;
653    }
654
655    @Override
656    public void deleteNotificationChannel(String pkg, int uid, String channelId) {
657        Preconditions.checkNotNull(pkg);
658        Preconditions.checkNotNull(channelId);
659        Record r = getRecord(pkg, uid);
660        if (r == null) {
661            return;
662        }
663        NotificationChannel channel = r.channels.get(channelId);
664        if (channel != null) {
665            channel.setDeleted(true);
666        }
667    }
668
669    @Override
670    @VisibleForTesting
671    public void permanentlyDeleteNotificationChannel(String pkg, int uid, String channelId) {
672        Preconditions.checkNotNull(pkg);
673        Preconditions.checkNotNull(channelId);
674        Record r = getRecord(pkg, uid);
675        if (r == null) {
676            return;
677        }
678        r.channels.remove(channelId);
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    }
696
697    public NotificationChannelGroup getNotificationChannelGroup(String groupId, String pkg,
698            int uid) {
699        Preconditions.checkNotNull(pkg);
700        Record r = getRecord(pkg, uid);
701        return r.groups.get(groupId);
702    }
703
704    @Override
705    public ParceledListSlice<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
706            int uid, boolean includeDeleted) {
707        Preconditions.checkNotNull(pkg);
708        Map<String, NotificationChannelGroup> groups = new ArrayMap<>();
709        Record r = getRecord(pkg, uid);
710        if (r == null) {
711            return ParceledListSlice.emptyList();
712        }
713        NotificationChannelGroup nonGrouped = new NotificationChannelGroup(null, null);
714        int N = r.channels.size();
715        for (int i = 0; i < N; i++) {
716            final NotificationChannel nc = r.channels.valueAt(i);
717            if (includeDeleted || !nc.isDeleted()) {
718                if (nc.getGroup() != null) {
719                    NotificationChannelGroup ncg = groups.get(nc.getGroup());
720                    if (ncg == null ) {
721                        ncg = r.groups.get(nc.getGroup()).clone();
722                        groups.put(nc.getGroup(), ncg);
723                    }
724                    ncg.addChannel(nc);
725                } else {
726                    nonGrouped.addChannel(nc);
727                }
728            }
729        }
730        if (nonGrouped.getChannels().size() > 0) {
731            groups.put(null, nonGrouped);
732        }
733        return new ParceledListSlice<>(new ArrayList<>(groups.values()));
734    }
735
736    @Override
737    @VisibleForTesting
738    public Collection<NotificationChannelGroup> getNotificationChannelGroups(String pkg,
739            int uid) {
740        Record r = getRecord(pkg, uid);
741        if (r == null) {
742            return new ArrayList<>();
743        }
744        return r.groups.values();
745    }
746
747    @Override
748    public ParceledListSlice<NotificationChannel> getNotificationChannels(String pkg, int uid,
749            boolean includeDeleted) {
750        Preconditions.checkNotNull(pkg);
751        List<NotificationChannel> channels = new ArrayList<>();
752        Record r = getRecord(pkg, uid);
753        if (r == null) {
754            return ParceledListSlice.emptyList();
755        }
756        int N = r.channels.size();
757        for (int i = 0; i < N; i++) {
758            final NotificationChannel nc = r.channels.valueAt(i);
759            if (includeDeleted || !nc.isDeleted()) {
760                channels.add(nc);
761            }
762        }
763        return new ParceledListSlice<>(channels);
764    }
765
766    /**
767     * Sets importance.
768     */
769    @Override
770    public void setImportance(String pkgName, int uid, int importance) {
771        getOrCreateRecord(pkgName, uid).importance = importance;
772        updateConfig();
773    }
774
775    public void setEnabled(String packageName, int uid, boolean enabled) {
776        boolean wasEnabled = getImportance(packageName, uid) != NotificationManager.IMPORTANCE_NONE;
777        if (wasEnabled == enabled) {
778            return;
779        }
780        setImportance(packageName, uid,
781                enabled ? DEFAULT_IMPORTANCE : NotificationManager.IMPORTANCE_NONE);
782    }
783
784    public void dump(PrintWriter pw, String prefix, NotificationManagerService.DumpFilter filter) {
785        if (filter == null) {
786            final int N = mSignalExtractors.length;
787            pw.print(prefix);
788            pw.print("mSignalExtractors.length = ");
789            pw.println(N);
790            for (int i = 0; i < N; i++) {
791                pw.print(prefix);
792                pw.print("  ");
793                pw.println(mSignalExtractors[i]);
794            }
795        }
796        if (filter == null) {
797            pw.print(prefix);
798            pw.println("per-package config:");
799        }
800        pw.println("Records:");
801        dumpRecords(pw, prefix, filter, mRecords);
802        pw.println("Restored without uid:");
803        dumpRecords(pw, prefix, filter, mRestoredWithoutUids);
804    }
805
806    private static void dumpRecords(PrintWriter pw, String prefix,
807            NotificationManagerService.DumpFilter filter, ArrayMap<String, Record> records) {
808        final int N = records.size();
809        for (int i = 0; i < N; i++) {
810            final Record r = records.valueAt(i);
811            if (filter == null || filter.matches(r.pkg)) {
812                pw.print(prefix);
813                pw.print("  AppSettings: ");
814                pw.print(r.pkg);
815                pw.print(" (");
816                pw.print(r.uid == Record.UNKNOWN_UID ? "UNKNOWN_UID" : Integer.toString(r.uid));
817                pw.print(')');
818                if (r.importance != DEFAULT_IMPORTANCE) {
819                    pw.print(" importance=");
820                    pw.print(Ranking.importanceToString(r.importance));
821                }
822                if (r.priority != DEFAULT_PRIORITY) {
823                    pw.print(" priority=");
824                    pw.print(Notification.priorityToString(r.priority));
825                }
826                if (r.visibility != DEFAULT_VISIBILITY) {
827                    pw.print(" visibility=");
828                    pw.print(Notification.visibilityToString(r.visibility));
829                }
830                pw.print(" showBadge=");
831                pw.print(Boolean.toString(r.showBadge));
832                pw.println();
833                for (NotificationChannel channel : r.channels.values()) {
834                    pw.print(prefix);
835                    pw.print("  ");
836                    pw.print("  ");
837                    pw.println(channel);
838                }
839                for (NotificationChannelGroup group : r.groups.values()) {
840                    pw.print(prefix);
841                    pw.print("  ");
842                    pw.print("  ");
843                    pw.println(group);
844                }
845            }
846        }
847    }
848
849    public JSONObject dumpJson(NotificationManagerService.DumpFilter filter) {
850        JSONObject ranking = new JSONObject();
851        JSONArray records = new JSONArray();
852        try {
853            ranking.put("noUid", mRestoredWithoutUids.size());
854        } catch (JSONException e) {
855           // pass
856        }
857        final int N = mRecords.size();
858        for (int i = 0; i < N; i++) {
859            final Record r = mRecords.valueAt(i);
860            if (filter == null || filter.matches(r.pkg)) {
861                JSONObject record = new JSONObject();
862                try {
863                    record.put("userId", UserHandle.getUserId(r.uid));
864                    record.put("packageName", r.pkg);
865                    if (r.importance != DEFAULT_IMPORTANCE) {
866                        record.put("importance", Ranking.importanceToString(r.importance));
867                    }
868                    if (r.priority != DEFAULT_PRIORITY) {
869                        record.put("priority", Notification.priorityToString(r.priority));
870                    }
871                    if (r.visibility != DEFAULT_VISIBILITY) {
872                        record.put("visibility", Notification.visibilityToString(r.visibility));
873                    }
874                    if (r.showBadge != DEFAULT_SHOW_BADGE) {
875                        record.put("showBadge", Boolean.valueOf(r.showBadge));
876                    }
877                    for (NotificationChannel channel : r.channels.values()) {
878                        record.put("channel", channel.toJson());
879                    }
880                    for (NotificationChannelGroup group : r.groups.values()) {
881                        record.put("group", group.toJson());
882                    }
883                } catch (JSONException e) {
884                   // pass
885                }
886                records.put(record);
887            }
888        }
889        try {
890            ranking.put("records", records);
891        } catch (JSONException e) {
892            // pass
893        }
894        return ranking;
895    }
896
897    /**
898     * Dump only the ban information as structured JSON for the stats collector.
899     *
900     * This is intentionally redundant with {#link dumpJson} because the old
901     * scraper will expect this format.
902     *
903     * @param filter
904     * @return
905     */
906    public JSONArray dumpBansJson(NotificationManagerService.DumpFilter filter) {
907        JSONArray bans = new JSONArray();
908        Map<Integer, String> packageBans = getPackageBans();
909        for(Entry<Integer, String> ban : packageBans.entrySet()) {
910            final int userId = UserHandle.getUserId(ban.getKey());
911            final String packageName = ban.getValue();
912            if (filter == null || filter.matches(packageName)) {
913                JSONObject banJson = new JSONObject();
914                try {
915                    banJson.put("userId", userId);
916                    banJson.put("packageName", packageName);
917                } catch (JSONException e) {
918                    e.printStackTrace();
919                }
920                bans.put(banJson);
921            }
922        }
923        return bans;
924    }
925
926    public Map<Integer, String> getPackageBans() {
927        final int N = mRecords.size();
928        ArrayMap<Integer, String> packageBans = new ArrayMap<>(N);
929        for (int i = 0; i < N; i++) {
930            final Record r = mRecords.valueAt(i);
931            if (r.importance == NotificationManager.IMPORTANCE_NONE) {
932                packageBans.put(r.uid, r.pkg);
933            }
934        }
935        return packageBans;
936    }
937
938    public void onPackagesChanged(boolean removingPackage, int changeUserId, String[] pkgList,
939            int[] uidList) {
940        if (pkgList == null || pkgList.length == 0) {
941            return; // nothing to do
942        }
943        boolean updated = false;
944        if (removingPackage) {
945            // Remove notification settings for uninstalled package
946            int size = Math.min(pkgList.length, uidList.length);
947            for (int i = 0; i < size; i++) {
948                final String pkg = pkgList[i];
949                final int uid = uidList[i];
950                mRecords.remove(recordKey(pkg, uid));
951                mRestoredWithoutUids.remove(pkg);
952                updated = true;
953            }
954        } else {
955            for (String pkg : pkgList) {
956                // Package install
957                final Record r = mRestoredWithoutUids.get(pkg);
958                if (r != null) {
959                    try {
960                        r.uid = mPm.getPackageUidAsUser(r.pkg, changeUserId);
961                        mRestoredWithoutUids.remove(pkg);
962                        mRecords.put(recordKey(r.pkg, r.uid), r);
963                        updated = true;
964                    } catch (NameNotFoundException e) {
965                        // noop
966                    }
967                }
968                // Package upgrade
969                try {
970                    Record fullRecord = getRecord(pkg,
971                            mPm.getPackageUidAsUser(pkg, changeUserId));
972                    if (fullRecord != null) {
973                        clampDefaultChannel(fullRecord);
974                    }
975                } catch (NameNotFoundException e) {
976                }
977            }
978        }
979
980        if (updated) {
981            updateConfig();
982        }
983    }
984
985    private static class Record {
986        static int UNKNOWN_UID = UserHandle.USER_NULL;
987
988        String pkg;
989        int uid = UNKNOWN_UID;
990        int importance = DEFAULT_IMPORTANCE;
991        int priority = DEFAULT_PRIORITY;
992        int visibility = DEFAULT_VISIBILITY;
993        boolean showBadge = DEFAULT_SHOW_BADGE;
994
995        ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
996        ArrayMap<String, NotificationChannelGroup> groups = new ArrayMap<>();
997   }
998}
999