1/*
2 * Copyright (C) 2016 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 */
16
17package android.ext.services.notification;
18
19import static android.service.notification.NotificationListenerService.Ranking.IMPORTANCE_UNSPECIFIED;
20
21import android.os.Bundle;
22import android.os.UserHandle;
23import android.service.notification.Adjustment;
24import android.service.notification.NotificationRankerService;
25import android.service.notification.StatusBarNotification;
26import android.util.Log;
27import android.util.Slog;
28
29import java.util.ArrayList;
30import java.util.HashMap;
31import java.util.LinkedHashSet;
32import java.util.List;
33import java.util.Map;
34
35import android.ext.services.R;
36
37/**
38 * Class that provides an updatable ranker module for the notification manager..
39 */
40public final class Ranker extends NotificationRankerService {
41    private static final String TAG = "RocketRanker";
42    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
43
44    private static final int AUTOBUNDLE_AT_COUNT = 4;
45    private static final String AUTOBUNDLE_KEY = "ranker_bundle";
46
47    // Map of user : <Map of package : notification keys>. Only contains notifications that are not
48    // bundled by the app (aka no group or sort key).
49    Map<Integer, Map<String, LinkedHashSet<String>>> mUnbundledNotifications;
50
51    @Override
52    public Adjustment onNotificationEnqueued(StatusBarNotification sbn, int importance,
53            boolean user) {
54        if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
55        return null;
56    }
57
58    @Override
59    public void onNotificationPosted(StatusBarNotification sbn) {
60        if (DEBUG) Log.i(TAG, "POSTED " + sbn.getKey());
61        try {
62            List<String> notificationsToBundle = new ArrayList<>();
63            if (!sbn.isAppGroup()) {
64                // Not grouped by the app, add to the list of notifications for the app;
65                // send bundling update if app exceeds the autobundling limit.
66                synchronized (mUnbundledNotifications) {
67                    Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
68                            = mUnbundledNotifications.get(sbn.getUserId());
69                    if (unbundledNotificationsByUser == null) {
70                        unbundledNotificationsByUser = new HashMap<>();
71                    }
72                    mUnbundledNotifications.put(sbn.getUserId(), unbundledNotificationsByUser);
73                    LinkedHashSet<String> notificationsForPackage
74                            = unbundledNotificationsByUser.get(sbn.getPackageName());
75                    if (notificationsForPackage == null) {
76                        notificationsForPackage = new LinkedHashSet<>();
77                    }
78
79                    notificationsForPackage.add(sbn.getKey());
80                    unbundledNotificationsByUser.put(sbn.getPackageName(), notificationsForPackage);
81
82                    if (notificationsForPackage.size() >= AUTOBUNDLE_AT_COUNT) {
83                        for (String key : notificationsForPackage) {
84                            notificationsToBundle.add(key);
85                        }
86                    }
87                }
88                if (notificationsToBundle.size() > 0) {
89                    adjustAutobundlingSummary(sbn.getPackageName(), notificationsToBundle.get(0),
90                            true, sbn.getUserId());
91                    adjustNotificationBundling(sbn.getPackageName(), notificationsToBundle, true,
92                            sbn.getUserId());
93                }
94            } else {
95                // Grouped, but not by us. Send updates to unautobundle, if we bundled it.
96                maybeUnbundle(sbn, false, sbn.getUserId());
97            }
98        } catch (Exception e) {
99            Slog.e(TAG, "Failure processing new notification", e);
100        }
101    }
102
103    @Override
104    public void onNotificationRemoved(StatusBarNotification sbn) {
105        try {
106            maybeUnbundle(sbn, true, sbn.getUserId());
107        } catch (Exception e) {
108            Slog.e(TAG, "Error processing canceled notification", e);
109        }
110    }
111
112    /**
113     * Un-autobundles notifications that are now grouped by the app. Additionally cancels
114     * autobundling if the status change of this notification resulted in the loose notification
115     * count being under the limit.
116     */
117    private void maybeUnbundle(StatusBarNotification sbn, boolean notificationGone, int user) {
118        List<String> notificationsToUnAutobundle = new ArrayList<>();
119        boolean removeSummary = false;
120        synchronized (mUnbundledNotifications) {
121            Map<String, LinkedHashSet<String>> unbundledNotificationsByUser
122                    = mUnbundledNotifications.get(sbn.getUserId());
123            if (unbundledNotificationsByUser == null || unbundledNotificationsByUser.size() == 0) {
124                return;
125            }
126            LinkedHashSet<String> notificationsForPackage
127                    = unbundledNotificationsByUser.get(sbn.getPackageName());
128            if (notificationsForPackage == null || notificationsForPackage.size() == 0) {
129                return;
130            }
131            if (notificationsForPackage.remove(sbn.getKey())) {
132                if (!notificationGone) {
133                    // Add the current notification to the unbundling list if it still exists.
134                    notificationsToUnAutobundle.add(sbn.getKey());
135                }
136                // If the status change of this notification has brought the number of loose
137                // notifications back below the limit, remove the summary and un-autobundle.
138                if (notificationsForPackage.size() == AUTOBUNDLE_AT_COUNT - 1) {
139                    removeSummary = true;
140                    for (String key : notificationsForPackage) {
141                        notificationsToUnAutobundle.add(key);
142                    }
143                }
144            }
145        }
146        if (notificationsToUnAutobundle.size() > 0) {
147            if (removeSummary) {
148                adjustAutobundlingSummary(sbn.getPackageName(), null, false, user);
149            }
150            adjustNotificationBundling(sbn.getPackageName(), notificationsToUnAutobundle, false,
151                    user);
152        }
153    }
154
155    @Override
156    public void onListenerConnected() {
157        if (DEBUG) Log.i(TAG, "CONNECTED");
158        mUnbundledNotifications = new HashMap<>();
159        for (StatusBarNotification sbn : getActiveNotifications()) {
160            onNotificationPosted(sbn);
161        }
162    }
163
164    private void adjustAutobundlingSummary(String packageName, String key, boolean summaryNeeded,
165            int user) {
166        Bundle signals = new Bundle();
167        if (summaryNeeded) {
168            signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, true);
169            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
170        } else {
171            signals.putBoolean(Adjustment.NEEDS_AUTOGROUPING_KEY, false);
172        }
173        Adjustment adjustment = new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
174                getContext().getString(R.string.notification_ranker_autobundle_explanation), null,
175                user);
176        if (DEBUG) {
177            Log.i(TAG, "Summary update for: " + packageName + " "
178                    + (summaryNeeded ? "adding" : "removing"));
179        }
180        try {
181            adjustNotification(adjustment);
182        } catch (Exception e) {
183            Slog.e(TAG, "Adjustment failed", e);
184        }
185
186    }
187    private void adjustNotificationBundling(String packageName, List<String> keys, boolean bundle,
188            int user) {
189        List<Adjustment> adjustments = new ArrayList<>();
190        for (String key : keys) {
191            adjustments.add(createBundlingAdjustment(packageName, key, bundle, user));
192            if (DEBUG) Log.i(TAG, "Sending bundling adjustment for: " + key);
193        }
194        try {
195            adjustNotifications(adjustments);
196        } catch (Exception e) {
197            Slog.e(TAG, "Adjustments failed", e);
198        }
199    }
200
201    private Adjustment createBundlingAdjustment(String packageName, String key, boolean bundle,
202            int user) {
203        Bundle signals = new Bundle();
204        if (bundle) {
205            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, AUTOBUNDLE_KEY);
206        } else {
207            signals.putString(Adjustment.GROUP_KEY_OVERRIDE_KEY, null);
208        }
209        return new Adjustment(packageName, key, IMPORTANCE_UNSPECIFIED, signals,
210                getContext().getString(R.string.notification_ranker_autobundle_explanation),
211                null, user);
212    }
213
214}