Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 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 | */ |
Rohan Shah | 20790b8 | 2018-07-02 17:21:04 -0700 | [diff] [blame] | 16 | package com.android.systemui.statusbar.notification.logging; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 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; |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 24 | import android.service.notification.NotificationStats; |
| 25 | import android.service.notification.StatusBarNotification; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 26 | import android.util.ArrayMap; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 27 | import android.util.ArraySet; |
| 28 | import android.util.Log; |
| 29 | |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 30 | import androidx.annotation.Nullable; |
| 31 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 32 | import com.android.internal.annotations.VisibleForTesting; |
| 33 | import com.android.internal.statusbar.IStatusBarService; |
| 34 | import com.android.internal.statusbar.NotificationVisibility; |
| 35 | import com.android.systemui.UiOffloadThread; |
Beverly | 8fdb533 | 2019-02-04 14:29:49 -0500 | [diff] [blame] | 36 | import com.android.systemui.plugins.statusbar.StatusBarStateController; |
| 37 | import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener; |
Rohan Shah | 20790b8 | 2018-07-02 17:21:04 -0700 | [diff] [blame] | 38 | import com.android.systemui.statusbar.NotificationListener; |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 39 | import com.android.systemui.statusbar.notification.NotificationEntryListener; |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 40 | import com.android.systemui.statusbar.notification.NotificationEntryManager; |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 41 | import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
Gustav Sennton | f892fe9 | 2019-01-22 15:31:42 +0000 | [diff] [blame] | 42 | import com.android.systemui.statusbar.notification.stack.ExpandableViewState; |
Rohan Shah | 20790b8 | 2018-07-02 17:21:04 -0700 | [diff] [blame] | 43 | import com.android.systemui.statusbar.notification.stack.NotificationListContainer; |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 44 | import com.android.systemui.statusbar.policy.HeadsUpManager; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 45 | |
| 46 | import java.util.ArrayList; |
| 47 | import java.util.Collection; |
| 48 | import java.util.Collections; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 49 | import java.util.Map; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 50 | |
Jason Monk | 27d01a62 | 2018-12-10 15:57:09 -0500 | [diff] [blame] | 51 | import javax.inject.Inject; |
| 52 | import javax.inject.Singleton; |
| 53 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 54 | /** |
| 55 | * Handles notification logging, in particular, logging which notifications are visible and which |
| 56 | * are not. |
| 57 | */ |
Jason Monk | 27d01a62 | 2018-12-10 15:57:09 -0500 | [diff] [blame] | 58 | @Singleton |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 59 | public class NotificationLogger implements StateListener { |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 60 | 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 Courtney | 6c313d3 | 2017-12-14 19:57:51 +0900 | [diff] [blame] | 68 | |
| 69 | // Dependencies: |
Jason Monk | d97204c | 2018-12-21 15:49:04 -0500 | [diff] [blame] | 70 | private final NotificationListenerService mNotificationListener; |
| 71 | private final UiOffloadThread mUiOffloadThread; |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 72 | private final NotificationEntryManager mEntryManager; |
| 73 | private HeadsUpManager mHeadsUpManager; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 74 | private final ExpansionStateLogger mExpansionStateLogger; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 75 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 76 | protected Handler mHandler = new Handler(); |
| 77 | protected IStatusBarService mBarService; |
| 78 | private long mLastVisibilityReportUptimeMs; |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 79 | private NotificationListContainer mListContainer; |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 80 | private final Object mDozingLock = new Object(); |
Julia Reynolds | 6a63d1b | 2018-08-14 16:59:33 -0400 | [diff] [blame] | 81 | private boolean mDozing; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 82 | |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 83 | protected final OnChildLocationsChangedListener mNotificationLocationsChangedListener = |
| 84 | new OnChildLocationsChangedListener() { |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 85 | @Override |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 86 | public void onChildLocationsChanged() { |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 87 | 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 Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 124 | ArrayList<NotificationEntry> activeNotifications = mEntryManager |
Eliot Courtney | 4a96b36 | 2017-12-14 19:38:52 +0900 | [diff] [blame] | 125 | .getNotificationData().getActiveNotifications(); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 126 | int N = activeNotifications.size(); |
| 127 | for (int i = 0; i < N; i++) { |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 128 | NotificationEntry entry = activeNotifications.get(i); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 129 | String key = entry.notification.getKey(); |
Evan Laird | 9449285 | 2018-10-25 13:43:01 -0400 | [diff] [blame] | 130 | boolean isVisible = mListContainer.isInVisibleLocation(entry); |
Gustav Sennton | f892fe9 | 2019-01-22 15:31:42 +0000 | [diff] [blame] | 131 | NotificationVisibility visObj = NotificationVisibility.obtain(key, i, N, isVisible, |
| 132 | getNotificationLocation(entry)); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 133 | 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 Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 154 | mExpansionStateLogger.onVisibilityChanged( |
| 155 | mTmpCurrentlyVisibleNotifications, mTmpCurrentlyVisibleNotifications); |
| 156 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 157 | recycleAllVisibilityObjects(mTmpNoLongerVisibleNotifications); |
| 158 | mTmpCurrentlyVisibleNotifications.clear(); |
| 159 | mTmpNewlyVisibleNotifications.clear(); |
| 160 | mTmpNoLongerVisibleNotifications.clear(); |
| 161 | } |
| 162 | }; |
| 163 | |
Gustav Sennton | f892fe9 | 2019-01-22 15:31:42 +0000 | [diff] [blame] | 164 | /** |
| 165 | * Returns the location of the notification referenced by the given {@link NotificationEntry}. |
| 166 | */ |
| 167 | public static NotificationVisibility.NotificationLocation getNotificationLocation( |
| 168 | NotificationEntry entry) { |
Selim Cinek | 0b054d1 | 2019-01-24 11:58:23 -0800 | [diff] [blame] | 169 | if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) { |
Gustav Sennton | f892fe9 | 2019-01-22 15:31:42 +0000 | [diff] [blame] | 170 | return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN; |
| 171 | } |
Selim Cinek | 0b054d1 | 2019-01-24 11:58:23 -0800 | [diff] [blame] | 172 | return convertNotificationLocation(entry.getRow().getViewState().location); |
Gustav Sennton | f892fe9 | 2019-01-22 15:31:42 +0000 | [diff] [blame] | 173 | } |
| 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 Monk | 27d01a62 | 2018-12-10 15:57:09 -0500 | [diff] [blame] | 195 | @Inject |
Jason Monk | d97204c | 2018-12-21 15:49:04 -0500 | [diff] [blame] | 196 | public NotificationLogger(NotificationListener notificationListener, |
| 197 | UiOffloadThread uiOffloadThread, |
| 198 | NotificationEntryManager entryManager, |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 199 | StatusBarStateController statusBarStateController, |
| 200 | ExpansionStateLogger expansionStateLogger) { |
Jason Monk | d97204c | 2018-12-21 15:49:04 -0500 | [diff] [blame] | 201 | mNotificationListener = notificationListener; |
| 202 | mUiOffloadThread = uiOffloadThread; |
| 203 | mEntryManager = entryManager; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 204 | mBarService = IStatusBarService.Stub.asInterface( |
| 205 | ServiceManager.getService(Context.STATUS_BAR_SERVICE)); |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 206 | mExpansionStateLogger = expansionStateLogger; |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 207 | // Not expected to be destroyed, don't need to unsubscribe |
Jason Monk | d97204c | 2018-12-21 15:49:04 -0500 | [diff] [blame] | 208 | statusBarStateController.addCallback(this); |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 209 | |
| 210 | entryManager.addNotificationEntryListener(new NotificationEntryListener() { |
| 211 | @Override |
| 212 | public void onEntryRemoved( |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 213 | NotificationEntry entry, |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 214 | NotificationVisibility visibility, |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 215 | boolean removedByUser) { |
Ned Burns | ef2ef6c | 2019-01-02 16:48:08 -0500 | [diff] [blame] | 216 | if (removedByUser && visibility != null) { |
| 217 | logNotificationClear(entry.key, entry.notification, visibility); |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 218 | } |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 219 | mExpansionStateLogger.onEntryRemoved(entry.key); |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 220 | } |
| 221 | |
| 222 | @Override |
Tony Mak | 96b3f1b | 2019-01-23 20:57:08 +0000 | [diff] [blame] | 223 | public void onEntryReinflated(NotificationEntry entry) { |
| 224 | mExpansionStateLogger.onEntryReinflated(entry.key); |
| 225 | } |
| 226 | |
| 227 | @Override |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 228 | public void onInflationError( |
| 229 | StatusBarNotification notification, |
| 230 | Exception exception) { |
| 231 | logNotificationError(notification, exception); |
| 232 | } |
| 233 | }); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 234 | } |
| 235 | |
Jason Monk | 297c04e | 2018-08-23 17:16:59 -0400 | [diff] [blame] | 236 | public void setUpWithContainer(NotificationListContainer listContainer) { |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 237 | mListContainer = listContainer; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 238 | } |
| 239 | |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 240 | public void setHeadsUpManager(HeadsUpManager headsUpManager) { |
| 241 | mHeadsUpManager = headsUpManager; |
| 242 | } |
| 243 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 244 | 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 Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 253 | mListContainer.setChildLocationsChangedListener(null); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 254 | } |
| 255 | |
| 256 | public void startNotificationLogging() { |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 257 | mListContainer.setChildLocationsChangedListener(mNotificationLocationsChangedListener); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 258 | // 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 Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 264 | mNotificationLocationsChangedListener.onChildLocationsChanged(); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 265 | } |
| 266 | |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 267 | private void setDozing(boolean dozing) { |
Julia Reynolds | 6a63d1b | 2018-08-14 16:59:33 -0400 | [diff] [blame] | 268 | synchronized (mDozingLock) { |
| 269 | mDozing = dozing; |
| 270 | } |
| 271 | } |
| 272 | |
Mady Mellor | c2ff011 | 2019-03-28 14:18:06 -0700 | [diff] [blame] | 273 | // TODO: This method has side effects, it is NOT just logging that a notification |
| 274 | // was cleared, it also actually removes the notification |
Gus Prevas | ca1b6f7 | 2018-12-28 10:53:11 -0500 | [diff] [blame] | 275 | 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 Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 314 | private void logNotificationVisibilityChanges( |
| 315 | Collection<NotificationVisibility> newlyVisible, |
| 316 | Collection<NotificationVisibility> noLongerVisible) { |
| 317 | if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { |
| 318 | return; |
| 319 | } |
Chris Wren | 2e89e8d | 2018-05-17 18:55:42 -0400 | [diff] [blame] | 320 | final NotificationVisibility[] newlyVisibleAr = cloneVisibilitiesAsArr(newlyVisible); |
| 321 | final NotificationVisibility[] noLongerVisibleAr = cloneVisibilitiesAsArr(noLongerVisible); |
| 322 | |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 323 | mUiOffloadThread.submit(() -> { |
| 324 | try { |
| 325 | mBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr); |
| 326 | } catch (RemoteException e) { |
| 327 | // Ignore. |
| 328 | } |
| 329 | |
Julia Reynolds | 6a63d1b | 2018-08-14 16:59:33 -0400 | [diff] [blame] | 330 | final int N = newlyVisibleAr.length; |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 331 | 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 Reynolds | 6a63d1b | 2018-08-14 16:59:33 -0400 | [diff] [blame] | 337 | 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 Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 349 | } |
| 350 | } |
Chris Wren | 2e89e8d | 2018-05-17 18:55:42 -0400 | [diff] [blame] | 351 | recycleAllVisibilityObjects(newlyVisibleAr); |
| 352 | recycleAllVisibilityObjects(noLongerVisibleAr); |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 353 | }); |
| 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 Wren | 2e89e8d | 2018-05-17 18:55:42 -0400 | [diff] [blame] | 364 | 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 Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 373 | private static NotificationVisibility[] cloneVisibilitiesAsArr( |
| 374 | Collection<NotificationVisibility> c) { |
Chris Wren | 2e89e8d | 2018-05-17 18:55:42 -0400 | [diff] [blame] | 375 | 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 Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 386 | @VisibleForTesting |
| 387 | public Runnable getVisibilityReporter() { |
| 388 | return mVisibilityReporter; |
| 389 | } |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 390 | |
Evan Laird | 878c853 | 2018-10-15 15:54:29 -0400 | [diff] [blame] | 391 | @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 Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 401 | /** |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 402 | * Called when the notification is expanded / collapsed. |
| 403 | */ |
| 404 | public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) { |
Gustav Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 405 | NotificationVisibility.NotificationLocation location = |
| 406 | getNotificationLocation(mEntryManager.getNotificationData().get(key)); |
| 407 | mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location); |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 408 | } |
| 409 | |
| 410 | /** |
Eliot Courtney | 2b4c3a0 | 2017-11-27 13:27:46 +0900 | [diff] [blame] | 411 | * A listener that is notified when some child locations might have changed. |
| 412 | */ |
| 413 | public interface OnChildLocationsChangedListener { |
| 414 | void onChildLocationsChanged(); |
| 415 | } |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 416 | |
| 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 Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 441 | void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded, |
| 442 | NotificationVisibility.NotificationLocation location) { |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 443 | State state = getState(key); |
| 444 | state.mIsUserAction = isUserAction; |
| 445 | state.mIsExpanded = isExpanded; |
Gustav Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 446 | state.mLocation = location; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 447 | 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 Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 462 | state.mLocation = nv.location; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 463 | 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 Mak | 96b3f1b | 2019-01-23 20:57:08 +0000 | [diff] [blame] | 477 | @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 Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 484 | 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 Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 514 | mBarService.onNotificationExpansionChanged(key, stateToBeLogged.mIsUserAction, |
| 515 | stateToBeLogged.mIsExpanded, stateToBeLogged.mLocation.ordinal()); |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 516 | } 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 Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 529 | @Nullable |
| 530 | NotificationVisibility.NotificationLocation mLocation; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 531 | |
| 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 Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 538 | this.mLocation = state.mLocation; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 539 | } |
| 540 | |
| 541 | private boolean isFullySet() { |
Gustav Sennton | c7d0d32 | 2019-01-07 15:36:41 +0000 | [diff] [blame] | 542 | return mIsUserAction != null && mIsExpanded != null && mIsVisible != null |
| 543 | && mLocation != null; |
Tony Mak | 202f25d | 2019-01-07 14:40:39 +0000 | [diff] [blame] | 544 | } |
| 545 | } |
| 546 | } |
Eliot Courtney | 3985ad5 | 2017-11-17 16:51:52 +0900 | [diff] [blame] | 547 | } |