1/*
2 * Copyright (C) 2017 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.systemui.statusbar;
17
18import android.content.Context;
19import android.os.Handler;
20import android.os.RemoteException;
21import android.os.ServiceManager;
22import android.os.SystemClock;
23import android.service.notification.NotificationListenerService;
24import android.util.ArraySet;
25import android.util.Log;
26
27import com.android.internal.annotations.VisibleForTesting;
28import com.android.internal.statusbar.IStatusBarService;
29import com.android.internal.statusbar.NotificationVisibility;
30import com.android.systemui.Dependency;
31import com.android.systemui.UiOffloadThread;
32
33import java.util.ArrayList;
34import java.util.Collection;
35import java.util.Collections;
36
37/**
38 * Handles notification logging, in particular, logging which notifications are visible and which
39 * are not.
40 */
41public class NotificationLogger {
42    private static final String TAG = "NotificationLogger";
43
44    /** The minimum delay in ms between reports of notification visibility. */
45    private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
46
47    /** Keys of notifications currently visible to the user. */
48    private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
49            new ArraySet<>();
50
51    // Dependencies:
52    private final NotificationListenerService mNotificationListener =
53            Dependency.get(NotificationListener.class);
54    private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
55
56    protected NotificationEntryManager mEntryManager;
57    protected Handler mHandler = new Handler();
58    protected IStatusBarService mBarService;
59    private long mLastVisibilityReportUptimeMs;
60    private NotificationListContainer mListContainer;
61
62    protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
63            new OnChildLocationsChangedListener() {
64                @Override
65                public void onChildLocationsChanged() {
66                    if (mHandler.hasCallbacks(mVisibilityReporter)) {
67                        // Visibilities will be reported when the existing
68                        // callback is executed.
69                        return;
70                    }
71                    // Calculate when we're allowed to run the visibility
72                    // reporter. Note that this timestamp might already have
73                    // passed. That's OK, the callback will just be executed
74                    // ASAP.
75                    long nextReportUptimeMs =
76                            mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
77                    mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
78                }
79            };
80
81    // Tracks notifications currently visible in mNotificationStackScroller and
82    // emits visibility events via NoMan on changes.
83    protected final Runnable mVisibilityReporter = new Runnable() {
84        private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
85                new ArraySet<>();
86        private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
87                new ArraySet<>();
88        private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
89                new ArraySet<>();
90
91        @Override
92        public void run() {
93            mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
94
95            // 1. Loop over mNotificationData entries:
96            //   A. Keep list of visible notifications.
97            //   B. Keep list of previously hidden, now visible notifications.
98            // 2. Compute no-longer visible notifications by removing currently
99            //    visible notifications from the set of previously visible
100            //    notifications.
101            // 3. Report newly visible and no-longer visible notifications.
102            // 4. Keep currently visible notifications for next report.
103            ArrayList<NotificationData.Entry> activeNotifications = mEntryManager
104                    .getNotificationData().getActiveNotifications();
105            int N = activeNotifications.size();
106            for (int i = 0; i < N; i++) {
107                NotificationData.Entry entry = activeNotifications.get(i);
108                String key = entry.notification.getKey();
109                boolean isVisible = mListContainer.isInVisibleLocation(entry.row);
110                NotificationVisibility visObj = NotificationVisibility.obtain(key, i, N, isVisible);
111                boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
112                if (isVisible) {
113                    // Build new set of visible notifications.
114                    mTmpCurrentlyVisibleNotifications.add(visObj);
115                    if (!previouslyVisible) {
116                        mTmpNewlyVisibleNotifications.add(visObj);
117                    }
118                } else {
119                    // release object
120                    visObj.recycle();
121                }
122            }
123            mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
124            mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
125
126            logNotificationVisibilityChanges(
127                    mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
128
129            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
130            mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
131
132            recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
133            mTmpCurrentlyVisibleNotifications.clear();
134            mTmpNewlyVisibleNotifications.clear();
135            mTmpNoLongerVisibleNotifications.clear();
136        }
137    };
138
139    public NotificationLogger() {
140        mBarService = IStatusBarService.Stub.asInterface(
141                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
142    }
143
144    public void setUpWithEntryManager(NotificationEntryManager entryManager,
145            NotificationListContainer listContainer) {
146        mEntryManager = entryManager;
147        mListContainer = listContainer;
148    }
149
150    public void stopNotificationLogging() {
151        // Report all notifications as invisible and turn down the
152        // reporter.
153        if (!mCurrentlyVisibleNotifications.isEmpty()) {
154            logNotificationVisibilityChanges(
155                    Collections.emptyList(), mCurrentlyVisibleNotifications);
156            recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
157        }
158        mHandler.removeCallbacks(mVisibilityReporter);
159        mListContainer.setChildLocationsChangedListener(null);
160    }
161
162    public void startNotificationLogging() {
163        mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
164        // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
165        // cause the scroller to emit child location events. Hence generate
166        // one ourselves to guarantee that we're reporting visible
167        // notifications.
168        // (Note that in cases where the scroller does emit events, this
169        // additional event doesn't break anything.)
170        mNotificationLocationsChangedListener.onChildLocationsChanged();
171    }
172
173    private void logNotificationVisibilityChanges(
174            Collection<NotificationVisibility> newlyVisible,
175            Collection<NotificationVisibility> noLongerVisible) {
176        if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
177            return;
178        }
179        final NotificationVisibility[] newlyVisibleAr = cloneVisibilitiesAsArr(newlyVisible);
180        final NotificationVisibility[] noLongerVisibleAr = cloneVisibilitiesAsArr(noLongerVisible);
181
182        mUiOffloadThread.submit(() -> {
183            try {
184                mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
185            } catch (RemoteException e) {
186                // Ignore.
187            }
188
189            final int N = newlyVisible.size();
190            if (N > 0) {
191                String[] newlyVisibleKeyAr = new String[N];
192                for (int i = 0; i < N; i++) {
193                    newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
194                }
195
196                // TODO: Call NotificationEntryManager to do this, once it exists.
197                // TODO: Consider not catching all runtime exceptions here.
198                try {
199                    mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
200                } catch (RuntimeException e) {
201                    Log.d(TAG, "failed setNotificationsShown: ", e);
202                }
203            }
204            recycleAllVisibilityObjects(newlyVisibleAr);
205            recycleAllVisibilityObjects(noLongerVisibleAr);
206        });
207    }
208
209    private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
210        final int N = array.size();
211        for (int i = 0 ; i < N; i++) {
212            array.valueAt(i).recycle();
213        }
214        array.clear();
215    }
216
217    private void recycleAllVisibilityObjects(NotificationVisibility[] array) {
218        final int N = array.length;
219        for (int i = 0 ; i < N; i++) {
220            if (array[i] != null) {
221                array[i].recycle();
222            }
223        }
224    }
225
226    private NotificationVisibility[] cloneVisibilitiesAsArr(Collection<NotificationVisibility> c) {
227
228        final NotificationVisibility[] array = new NotificationVisibility[c.size()];
229        int i = 0;
230        for(NotificationVisibility nv: c) {
231            if (nv != null) {
232                array[i] = nv.clone();
233            }
234            i++;
235        }
236        return array;
237    }
238
239    @VisibleForTesting
240    public Runnable getVisibilityReporter() {
241        return mVisibilityReporter;
242    }
243
244    /**
245     * A listener that is notified when some child locations might have changed.
246     */
247    public interface OnChildLocationsChangedListener {
248        void onChildLocationsChanged();
249    }
250}
251