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