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