blob: 94f7e651d73d3c5c98fdd0d2683d382487897a58 [file] [log] [blame]
Eliot Courtney3985ad52017-11-17 16:51:52 +09001/*
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 */
Rohan Shah20790b82018-07-02 17:21:04 -070016package com.android.systemui.statusbar.notification.logging;
Eliot Courtney3985ad52017-11-17 16:51:52 +090017
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;
Gus Prevasca1b6f72018-12-28 10:53:11 -050024import android.service.notification.NotificationStats;
25import android.service.notification.StatusBarNotification;
Tony Mak202f25d2019-01-07 14:40:39 +000026import android.util.ArrayMap;
Eliot Courtney3985ad52017-11-17 16:51:52 +090027import android.util.ArraySet;
28import android.util.Log;
29
Tony Mak202f25d2019-01-07 14:40:39 +000030import androidx.annotation.Nullable;
31
Eliot Courtney3985ad52017-11-17 16:51:52 +090032import com.android.internal.annotations.VisibleForTesting;
33import com.android.internal.statusbar.IStatusBarService;
34import com.android.internal.statusbar.NotificationVisibility;
35import com.android.systemui.UiOffloadThread;
Beverly8fdb5332019-02-04 14:29:49 -050036import com.android.systemui.plugins.statusbar.StatusBarStateController;
37import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
Rohan Shah20790b82018-07-02 17:21:04 -070038import com.android.systemui.statusbar.NotificationListener;
Gus Prevasca1b6f72018-12-28 10:53:11 -050039import com.android.systemui.statusbar.notification.NotificationEntryListener;
Evan Laird878c8532018-10-15 15:54:29 -040040import com.android.systemui.statusbar.notification.NotificationEntryManager;
Ned Burnsf81c4c42019-01-07 14:10:43 -050041import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Gustav Senntonf892fe92019-01-22 15:31:42 +000042import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
Rohan Shah20790b82018-07-02 17:21:04 -070043import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
Gus Prevasca1b6f72018-12-28 10:53:11 -050044import com.android.systemui.statusbar.policy.HeadsUpManager;
Eliot Courtney3985ad52017-11-17 16:51:52 +090045
46import java.util.ArrayList;
47import java.util.Collection;
48import java.util.Collections;
Tony Mak202f25d2019-01-07 14:40:39 +000049import java.util.Map;
Eliot Courtney3985ad52017-11-17 16:51:52 +090050
Jason Monk27d01a622018-12-10 15:57:09 -050051import javax.inject.Inject;
52import javax.inject.Singleton;
53
Eliot Courtney3985ad52017-11-17 16:51:52 +090054/**
55 * Handles notification logging, in particular, logging which notifications are visible and which
56 * are not.
57 */
Jason Monk27d01a622018-12-10 15:57:09 -050058@Singleton
Evan Laird878c8532018-10-15 15:54:29 -040059public class NotificationLogger implements StateListener {
Eliot Courtney3985ad52017-11-17 16:51:52 +090060 private static final String TAG = "NotificationLogger";
61
62 /** The minimum delay in ms between reports of notification visibility. */
63 private static final int VISIBILITY_REPORT_MIN_DELAY_MS = 500;
64
65 /** Keys of notifications currently visible to the user. */
66 private final ArraySet<NotificationVisibility> mCurrentlyVisibleNotifications =
67 new ArraySet<>();
Eliot Courtney6c313d32017-12-14 19:57:51 +090068
69 // Dependencies:
Jason Monkd97204c2018-12-21 15:49:04 -050070 private final NotificationListenerService mNotificationListener;
71 private final UiOffloadThread mUiOffloadThread;
Gus Prevasca1b6f72018-12-28 10:53:11 -050072 private final NotificationEntryManager mEntryManager;
73 private HeadsUpManager mHeadsUpManager;
Tony Mak202f25d2019-01-07 14:40:39 +000074 private final ExpansionStateLogger mExpansionStateLogger;
Eliot Courtney3985ad52017-11-17 16:51:52 +090075
Eliot Courtney3985ad52017-11-17 16:51:52 +090076 protected Handler mHandler = new Handler();
77 protected IStatusBarService mBarService;
78 private long mLastVisibilityReportUptimeMs;
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090079 private NotificationListContainer mListContainer;
Evan Laird878c8532018-10-15 15:54:29 -040080 private final Object mDozingLock = new Object();
Julia Reynolds6a63d1b2018-08-14 16:59:33 -040081 private boolean mDozing;
Eliot Courtney3985ad52017-11-17 16:51:52 +090082
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090083 protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener =
84 new OnChildLocationsChangedListener() {
Eliot Courtney3985ad52017-11-17 16:51:52 +090085 @Override
Eliot Courtney2b4c3a02017-11-27 13:27:46 +090086 public void onChildLocationsChanged() {
Eliot Courtney3985ad52017-11-17 16:51:52 +090087 if (mHandler.hasCallbacks(mVisibilityReporter)) {
88 // Visibilities will be reported when the existing
89 // callback is executed.
90 return;
91 }
92 // Calculate when we're allowed to run the visibility
93 // reporter. Note that this timestamp might already have
94 // passed. That's OK, the callback will just be executed
95 // ASAP.
96 long nextReportUptimeMs =
97 mLastVisibilityReportUptimeMs + VISIBILITY_REPORT_MIN_DELAY_MS;
98 mHandler.postAtTime(mVisibilityReporter, nextReportUptimeMs);
99 }
100 };
101
102 // Tracks notifications currently visible in mNotificationStackScroller and
103 // emits visibility events via NoMan on changes.
104 protected final Runnable mVisibilityReporter = new Runnable() {
105 private final ArraySet<NotificationVisibility> mTmpNewlyVisibleNotifications =
106 new ArraySet<>();
107 private final ArraySet<NotificationVisibility> mTmpCurrentlyVisibleNotifications =
108 new ArraySet<>();
109 private final ArraySet<NotificationVisibility> mTmpNoLongerVisibleNotifications =
110 new ArraySet<>();
111
112 @Override
113 public void run() {
114 mLastVisibilityReportUptimeMs = SystemClock.uptimeMillis();
115
116 // 1. Loop over mNotificationData entries:
117 // A. Keep list of visible notifications.
118 // B. Keep list of previously hidden, now visible notifications.
119 // 2. Compute no-longer visible notifications by removing currently
120 // visible notifications from the set of previously visible
121 // notifications.
122 // 3. Report newly visible and no-longer visible notifications.
123 // 4. Keep currently visible notifications for next report.
Ned Burnsf81c4c42019-01-07 14:10:43 -0500124 ArrayList<NotificationEntry> activeNotifications = mEntryManager
Eliot Courtney4a96b362017-12-14 19:38:52 +0900125 .getNotificationData().getActiveNotifications();
Eliot Courtney3985ad52017-11-17 16:51:52 +0900126 int N = activeNotifications.size();
127 for (int i = 0; i < N; i++) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500128 NotificationEntry entry = activeNotifications.get(i);
Eliot Courtney3985ad52017-11-17 16:51:52 +0900129 String key = entry.notification.getKey();
Evan Laird94492852018-10-25 13:43:01 -0400130 boolean isVisible = mListContainer.isInVisibleLocation(entry);
Gustav Senntonf892fe92019-01-22 15:31:42 +0000131 NotificationVisibility visObj = NotificationVisibility.obtain(key, i, N, isVisible,
132 getNotificationLocation(entry));
Eliot Courtney3985ad52017-11-17 16:51:52 +0900133 boolean previouslyVisible = mCurrentlyVisibleNotifications.contains(visObj);
134 if (isVisible) {
135 // Build new set of visible notifications.
136 mTmpCurrentlyVisibleNotifications.add(visObj);
137 if (!previouslyVisible) {
138 mTmpNewlyVisibleNotifications.add(visObj);
139 }
140 } else {
141 // release object
142 visObj.recycle();
143 }
144 }
145 mTmpNoLongerVisibleNotifications.addAll(mCurrentlyVisibleNotifications);
146 mTmpNoLongerVisibleNotifications.removeAll(mTmpCurrentlyVisibleNotifications);
147
148 logNotificationVisibilityChanges(
149 mTmpNewlyVisibleNotifications, mTmpNoLongerVisibleNotifications);
150
151 recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
152 mCurrentlyVisibleNotifications.addAll(mTmpCurrentlyVisibleNotifications);
153
Tony Mak202f25d2019-01-07 14:40:39 +0000154 mExpansionStateLogger.onVisibilityChanged(
155 mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications);
156
Eliot Courtney3985ad52017-11-17 16:51:52 +0900157 recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications);
158 mTmpCurrentlyVisibleNotifications.clear();
159 mTmpNewlyVisibleNotifications.clear();
160 mTmpNoLongerVisibleNotifications.clear();
161 }
162 };
163
Gustav Senntonf892fe92019-01-22 15:31:42 +0000164 /**
165 * Returns the location of the notification referenced by the given {@link NotificationEntry}.
166 */
167 public static NotificationVisibility.NotificationLocation getNotificationLocation(
168 NotificationEntry entry) {
Selim Cinek0b054d12019-01-24 11:58:23 -0800169 if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) {
Gustav Senntonf892fe92019-01-22 15:31:42 +0000170 return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
171 }
Selim Cinek0b054d12019-01-24 11:58:23 -0800172 return convertNotificationLocation(entry.getRow().getViewState().location);
Gustav Senntonf892fe92019-01-22 15:31:42 +0000173 }
174
175 private static NotificationVisibility.NotificationLocation convertNotificationLocation(
176 int location) {
177 switch (location) {
178 case ExpandableViewState.LOCATION_FIRST_HUN:
179 return NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP;
180 case ExpandableViewState.LOCATION_HIDDEN_TOP:
181 return NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP;
182 case ExpandableViewState.LOCATION_MAIN_AREA:
183 return NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA;
184 case ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING:
185 return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING;
186 case ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN:
187 return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN;
188 case ExpandableViewState.LOCATION_GONE:
189 return NotificationVisibility.NotificationLocation.LOCATION_GONE;
190 default:
191 return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN;
192 }
193 }
194
Jason Monk27d01a622018-12-10 15:57:09 -0500195 @Inject
Jason Monkd97204c2018-12-21 15:49:04 -0500196 public NotificationLogger(NotificationListener notificationListener,
197 UiOffloadThread uiOffloadThread,
198 NotificationEntryManager entryManager,
Tony Mak202f25d2019-01-07 14:40:39 +0000199 StatusBarStateController statusBarStateController,
200 ExpansionStateLogger expansionStateLogger) {
Jason Monkd97204c2018-12-21 15:49:04 -0500201 mNotificationListener = notificationListener;
202 mUiOffloadThread = uiOffloadThread;
203 mEntryManager = entryManager;
Eliot Courtney3985ad52017-11-17 16:51:52 +0900204 mBarService = IStatusBarService.Stub.asInterface(
205 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
Tony Mak202f25d2019-01-07 14:40:39 +0000206 mExpansionStateLogger = expansionStateLogger;
Evan Laird878c8532018-10-15 15:54:29 -0400207 // Not expected to be destroyed, don't need to unsubscribe
Jason Monkd97204c2018-12-21 15:49:04 -0500208 statusBarStateController.addCallback(this);
Gus Prevasca1b6f72018-12-28 10:53:11 -0500209
210 entryManager.addNotificationEntryListener(new NotificationEntryListener() {
211 @Override
212 public void onEntryRemoved(
Ned Burnsf81c4c42019-01-07 14:10:43 -0500213 NotificationEntry entry,
Gus Prevasca1b6f72018-12-28 10:53:11 -0500214 NotificationVisibility visibility,
Gus Prevasca1b6f72018-12-28 10:53:11 -0500215 boolean removedByUser) {
Ned Burnsef2ef6c2019-01-02 16:48:08 -0500216 if (removedByUser && visibility != null) {
217 logNotificationClear(entry.key, entry.notification, visibility);
Gus Prevasca1b6f72018-12-28 10:53:11 -0500218 }
Tony Mak202f25d2019-01-07 14:40:39 +0000219 mExpansionStateLogger.onEntryRemoved(entry.key);
Gus Prevasca1b6f72018-12-28 10:53:11 -0500220 }
221
222 @Override
Tony Mak96b3f1b2019-01-23 20:57:08 +0000223 public void onEntryReinflated(NotificationEntry entry) {
224 mExpansionStateLogger.onEntryReinflated(entry.key);
225 }
226
227 @Override
Gus Prevasca1b6f72018-12-28 10:53:11 -0500228 public void onInflationError(
229 StatusBarNotification notification,
230 Exception exception) {
231 logNotificationError(notification, exception);
232 }
233 });
Eliot Courtney3985ad52017-11-17 16:51:52 +0900234 }
235
Jason Monk297c04e2018-08-23 17:16:59 -0400236 public void setUpWithContainer(NotificationListContainer listContainer) {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900237 mListContainer = listContainer;
Eliot Courtney3985ad52017-11-17 16:51:52 +0900238 }
239
Gus Prevasca1b6f72018-12-28 10:53:11 -0500240 public void setHeadsUpManager(HeadsUpManager headsUpManager) {
241 mHeadsUpManager = headsUpManager;
242 }
243
Eliot Courtney3985ad52017-11-17 16:51:52 +0900244 public void stopNotificationLogging() {
245 // Report all notifications as invisible and turn down the
246 // reporter.
247 if (!mCurrentlyVisibleNotifications.isEmpty()) {
248 logNotificationVisibilityChanges(
249 Collections.emptyList(), mCurrentlyVisibleNotifications);
250 recycleAllVisibilityObjects(mCurrentlyVisibleNotifications);
251 }
252 mHandler.removeCallbacks(mVisibilityReporter);
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900253 mListContainer.setChildLocationsChangedListener(null);
Eliot Courtney3985ad52017-11-17 16:51:52 +0900254 }
255
256 public void startNotificationLogging() {
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900257 mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener);
Eliot Courtney3985ad52017-11-17 16:51:52 +0900258 // Some transitions like mVisibleToUser=false -> mVisibleToUser=true don't
259 // cause the scroller to emit child location events. Hence generate
260 // one ourselves to guarantee that we're reporting visible
261 // notifications.
262 // (Note that in cases where the scroller does emit events, this
263 // additional event doesn't break anything.)
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900264 mNotificationLocationsChangedListener.onChildLocationsChanged();
Eliot Courtney3985ad52017-11-17 16:51:52 +0900265 }
266
Evan Laird878c8532018-10-15 15:54:29 -0400267 private void setDozing(boolean dozing) {
Julia Reynolds6a63d1b2018-08-14 16:59:33 -0400268 synchronized (mDozingLock) {
269 mDozing = dozing;
270 }
271 }
272
Mady Mellorc2ff0112019-03-28 14:18:06 -0700273 // TODO: This method has side effects, it is NOT just logging that a notification
274 // was cleared, it also actually removes the notification
Gus Prevasca1b6f72018-12-28 10:53:11 -0500275 private void logNotificationClear(String key, StatusBarNotification notification,
276 NotificationVisibility nv) {
277 final String pkg = notification.getPackageName();
278 final String tag = notification.getTag();
279 final int id = notification.getId();
280 final int userId = notification.getUserId();
281 try {
282 int dismissalSurface = NotificationStats.DISMISSAL_SHADE;
283 if (mHeadsUpManager.isAlerting(key)) {
284 dismissalSurface = NotificationStats.DISMISSAL_PEEK;
285 } else if (mListContainer.hasPulsingNotifications()) {
286 dismissalSurface = NotificationStats.DISMISSAL_AOD;
287 }
288 int dismissalSentiment = NotificationStats.DISMISS_SENTIMENT_NEUTRAL;
289 mBarService.onNotificationClear(pkg, tag, id, userId, notification.getKey(),
290 dismissalSurface,
291 dismissalSentiment, nv);
292 } catch (RemoteException ex) {
293 // system process is dead if we're here.
294 }
295 }
296
297 private void logNotificationError(
298 StatusBarNotification notification,
299 Exception exception) {
300 try {
301 mBarService.onNotificationError(
302 notification.getPackageName(),
303 notification.getTag(),
304 notification.getId(),
305 notification.getUid(),
306 notification.getInitialPid(),
307 exception.getMessage(),
308 notification.getUserId());
309 } catch (RemoteException ex) {
310 // The end is nigh.
311 }
312 }
313
Eliot Courtney3985ad52017-11-17 16:51:52 +0900314 private void logNotificationVisibilityChanges(
315 Collection<NotificationVisibility> newlyVisible,
316 Collection<NotificationVisibility> noLongerVisible) {
317 if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) {
318 return;
319 }
Chris Wren2e89e8d2018-05-17 18:55:42 -0400320 final NotificationVisibility[] newlyVisibleAr = cloneVisibilitiesAsArr(newlyVisible);
321 final NotificationVisibility[] noLongerVisibleAr = cloneVisibilitiesAsArr(noLongerVisible);
322
Eliot Courtney3985ad52017-11-17 16:51:52 +0900323 mUiOffloadThread.submit(() -> {
324 try {
325 mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr);
326 } catch (RemoteException e) {
327 // Ignore.
328 }
329
Julia Reynolds6a63d1b2018-08-14 16:59:33 -0400330 final int N = newlyVisibleAr.length;
Eliot Courtney3985ad52017-11-17 16:51:52 +0900331 if (N > 0) {
332 String[] newlyVisibleKeyAr = new String[N];
333 for (int i = 0; i < N; i++) {
334 newlyVisibleKeyAr[i] = newlyVisibleAr[i].key;
335 }
336
Julia Reynolds6a63d1b2018-08-14 16:59:33 -0400337 synchronized (mDozingLock) {
338 // setNotificationsShown should only be called if we are confident that
339 // the user has seen the notification, aka not when ambient display is on
340 if (!mDozing) {
341 // TODO: Call NotificationEntryManager to do this, once it exists.
342 // TODO: Consider not catching all runtime exceptions here.
343 try {
344 mNotificationListener.setNotificationsShown(newlyVisibleKeyAr);
345 } catch (RuntimeException e) {
346 Log.d(TAG, "failed setNotificationsShown: ", e);
347 }
348 }
Eliot Courtney3985ad52017-11-17 16:51:52 +0900349 }
350 }
Chris Wren2e89e8d2018-05-17 18:55:42 -0400351 recycleAllVisibilityObjects(newlyVisibleAr);
352 recycleAllVisibilityObjects(noLongerVisibleAr);
Eliot Courtney3985ad52017-11-17 16:51:52 +0900353 });
354 }
355
356 private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
357 final int N = array.size();
358 for (int i = 0 ; i < N; i++) {
359 array.valueAt(i).recycle();
360 }
361 array.clear();
362 }
363
Chris Wren2e89e8d2018-05-17 18:55:42 -0400364 private void recycleAllVisibilityObjects(NotificationVisibility[] array) {
365 final int N = array.length;
366 for (int i = 0 ; i < N; i++) {
367 if (array[i] != null) {
368 array[i].recycle();
369 }
370 }
371 }
372
Tony Mak202f25d2019-01-07 14:40:39 +0000373 private static NotificationVisibility[] cloneVisibilitiesAsArr(
374 Collection<NotificationVisibility> c) {
Chris Wren2e89e8d2018-05-17 18:55:42 -0400375 final NotificationVisibility[] array = new NotificationVisibility[c.size()];
376 int i = 0;
377 for(NotificationVisibility nv: c) {
378 if (nv != null) {
379 array[i] = nv.clone();
380 }
381 i++;
382 }
383 return array;
384 }
385
Eliot Courtney3985ad52017-11-17 16:51:52 +0900386 @VisibleForTesting
387 public Runnable getVisibilityReporter() {
388 return mVisibilityReporter;
389 }
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900390
Evan Laird878c8532018-10-15 15:54:29 -0400391 @Override
392 public void onStateChanged(int newState) {
393 // don't care about state change
394 }
395
396 @Override
397 public void onDozingChanged(boolean isDozing) {
398 setDozing(isDozing);
399 }
400
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900401 /**
Tony Mak202f25d2019-01-07 14:40:39 +0000402 * Called when the notification is expanded / collapsed.
403 */
404 public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) {
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000405 NotificationVisibility.NotificationLocation location =
406 getNotificationLocation(mEntryManager.getNotificationData().get(key));
407 mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location);
Tony Mak202f25d2019-01-07 14:40:39 +0000408 }
409
410 /**
Eliot Courtney2b4c3a02017-11-27 13:27:46 +0900411 * A listener that is notified when some child locations might have changed.
412 */
413 public interface OnChildLocationsChangedListener {
414 void onChildLocationsChanged();
415 }
Tony Mak202f25d2019-01-07 14:40:39 +0000416
417 /**
418 * Logs the expansion state change when the notification is visible.
419 */
420 public static class ExpansionStateLogger {
421 /** Notification key -> state, should be accessed in UI offload thread only. */
422 private final Map<String, State> mExpansionStates = new ArrayMap<>();
423
424 /**
425 * Notification key -> last logged expansion state, should be accessed in UI thread only.
426 */
427 private final Map<String, Boolean> mLoggedExpansionState = new ArrayMap<>();
428 private final UiOffloadThread mUiOffloadThread;
429 @VisibleForTesting
430 IStatusBarService mBarService;
431
432 @Inject
433 public ExpansionStateLogger(UiOffloadThread uiOffloadThread) {
434 mUiOffloadThread = uiOffloadThread;
435 mBarService =
436 IStatusBarService.Stub.asInterface(
437 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
438 }
439
440 @VisibleForTesting
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000441 void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded,
442 NotificationVisibility.NotificationLocation location) {
Tony Mak202f25d2019-01-07 14:40:39 +0000443 State state = getState(key);
444 state.mIsUserAction = isUserAction;
445 state.mIsExpanded = isExpanded;
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000446 state.mLocation = location;
Tony Mak202f25d2019-01-07 14:40:39 +0000447 maybeNotifyOnNotificationExpansionChanged(key, state);
448 }
449
450 @VisibleForTesting
451 void onVisibilityChanged(
452 Collection<NotificationVisibility> newlyVisible,
453 Collection<NotificationVisibility> noLongerVisible) {
454 final NotificationVisibility[] newlyVisibleAr =
455 cloneVisibilitiesAsArr(newlyVisible);
456 final NotificationVisibility[] noLongerVisibleAr =
457 cloneVisibilitiesAsArr(noLongerVisible);
458
459 for (NotificationVisibility nv : newlyVisibleAr) {
460 State state = getState(nv.key);
461 state.mIsVisible = true;
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000462 state.mLocation = nv.location;
Tony Mak202f25d2019-01-07 14:40:39 +0000463 maybeNotifyOnNotificationExpansionChanged(nv.key, state);
464 }
465 for (NotificationVisibility nv : noLongerVisibleAr) {
466 State state = getState(nv.key);
467 state.mIsVisible = false;
468 }
469 }
470
471 @VisibleForTesting
472 void onEntryRemoved(String key) {
473 mExpansionStates.remove(key);
474 mLoggedExpansionState.remove(key);
475 }
476
Tony Mak96b3f1b2019-01-23 20:57:08 +0000477 @VisibleForTesting
478 void onEntryReinflated(String key) {
479 // When the notification is updated, we should consider the notification as not
480 // yet logged.
481 mLoggedExpansionState.remove(key);
482 }
483
Tony Mak202f25d2019-01-07 14:40:39 +0000484 private State getState(String key) {
485 State state = mExpansionStates.get(key);
486 if (state == null) {
487 state = new State();
488 mExpansionStates.put(key, state);
489 }
490 return state;
491 }
492
493 private void maybeNotifyOnNotificationExpansionChanged(final String key, State state) {
494 if (!state.isFullySet()) {
495 return;
496 }
497 if (!state.mIsVisible) {
498 return;
499 }
500 Boolean loggedExpansionState = mLoggedExpansionState.get(key);
501 // Consider notification is initially collapsed, so only expanded is logged in the
502 // first time.
503 if (loggedExpansionState == null && !state.mIsExpanded) {
504 return;
505 }
506 if (loggedExpansionState != null
507 && state.mIsExpanded == loggedExpansionState) {
508 return;
509 }
510 mLoggedExpansionState.put(key, state.mIsExpanded);
511 final State stateToBeLogged = new State(state);
512 mUiOffloadThread.submit(() -> {
513 try {
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000514 mBarService.onNotificationExpansionChanged(key, stateToBeLogged.mIsUserAction,
515 stateToBeLogged.mIsExpanded, stateToBeLogged.mLocation.ordinal());
Tony Mak202f25d2019-01-07 14:40:39 +0000516 } catch (RemoteException e) {
517 Log.e(TAG, "Failed to call onNotificationExpansionChanged: ", e);
518 }
519 });
520 }
521
522 private static class State {
523 @Nullable
524 Boolean mIsUserAction;
525 @Nullable
526 Boolean mIsExpanded;
527 @Nullable
528 Boolean mIsVisible;
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000529 @Nullable
530 NotificationVisibility.NotificationLocation mLocation;
Tony Mak202f25d2019-01-07 14:40:39 +0000531
532 private State() {}
533
534 private State(State state) {
535 this.mIsUserAction = state.mIsUserAction;
536 this.mIsExpanded = state.mIsExpanded;
537 this.mIsVisible = state.mIsVisible;
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000538 this.mLocation = state.mLocation;
Tony Mak202f25d2019-01-07 14:40:39 +0000539 }
540
541 private boolean isFullySet() {
Gustav Senntonc7d0d322019-01-07 15:36:41 +0000542 return mIsUserAction != null && mIsExpanded != null && mIsVisible != null
543 && mLocation != null;
Tony Mak202f25d2019-01-07 14:40:39 +0000544 }
545 }
546 }
Eliot Courtney3985ad52017-11-17 16:51:52 +0900547}