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