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  */
16 package com.android.systemui.statusbar;
17 
18 import android.content.Context;
19 import android.os.Handler;
20 import android.os.RemoteException;
21 import android.os.ServiceManager;
22 import android.os.SystemClock;
23 import android.service.notification.NotificationListenerService;
24 import android.util.ArraySet;
25 import android.util.Log;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.internal.statusbar.IStatusBarService;
29 import com.android.internal.statusbar.NotificationVisibility;
30 import com.android.systemui.Dependency;
31 import com.android.systemui.UiOffloadThread;
32 
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.Collections;
36 
37 /**
38  * Handles notification logging, in particular, logging which notifications are visible and which
39  * are not.
40  */
41 public 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 
NotificationLogger()139     public NotificationLogger() {
140         mBarService = IStatusBarService.Stub.asInterface(
141                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
142     }
143 
setUpWithEntryManager(NotificationEntryManager entryManager, NotificationListContainer listContainer)144     public void setUpWithEntryManager(NotificationEntryManager entryManager,
145             NotificationListContainer listContainer) {
146         mEntryManager = entryManager;
147         mListContainer = listContainer;
148     }
149 
stopNotificationLogging()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 
startNotificationLogging()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 
logNotificationVisibilityChanges( Collection<NotificationVisibility> newlyVisible, Collection<NotificationVisibility> noLongerVisible)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 
recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array)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 
recycleAllVisibilityObjects(NotificationVisibility[] array)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 
cloneVisibilitiesAsArr(Collection<NotificationVisibility> c)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
getVisibilityReporter()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 {
onChildLocationsChanged()248         void onChildLocationsChanged();
249     }
250 }
251