1 /* 2 * Copyright (C) 2016 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 17 package com.android.systemui.statusbar.notification.collection.legacy; 18 19 import android.os.Handler; 20 import android.os.SystemClock; 21 import android.view.View; 22 23 import androidx.collection.ArraySet; 24 25 import com.android.systemui.Dumpable; 26 import com.android.systemui.dagger.qualifiers.Main; 27 import com.android.systemui.keyguard.WakefulnessLifecycle; 28 import com.android.systemui.plugins.statusbar.StatusBarStateController; 29 import com.android.systemui.statusbar.notification.NotificationEntryListener; 30 import com.android.systemui.statusbar.notification.NotificationEntryManager; 31 import com.android.systemui.statusbar.notification.VisibilityLocationProvider; 32 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 33 import com.android.systemui.statusbar.notification.dagger.NotificationsModule; 34 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 35 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; 36 37 import java.io.FileDescriptor; 38 import java.io.PrintWriter; 39 import java.util.ArrayList; 40 41 /** 42 * A manager that ensures that notifications are visually stable. It will suppress reorderings 43 * and reorder at the right time when they are out of view. 44 */ 45 public class VisualStabilityManager implements OnHeadsUpChangedListener, Dumpable { 46 47 private static final long TEMPORARY_REORDERING_ALLOWED_DURATION = 1000; 48 49 private final ArrayList<Callback> mReorderingAllowedCallbacks = new ArrayList<>(); 50 private final ArraySet<Callback> mPersistentReorderingCallbacks = new ArraySet<>(); 51 private final ArrayList<Callback> mGroupChangesAllowedCallbacks = new ArrayList<>(); 52 private final ArraySet<Callback> mPersistentGroupCallbacks = new ArraySet<>(); 53 private final Handler mHandler; 54 55 private boolean mPanelExpanded; 56 private boolean mScreenOn; 57 private boolean mReorderingAllowed; 58 private boolean mGroupChangedAllowed; 59 private boolean mIsTemporaryReorderingAllowed; 60 private long mTemporaryReorderingStart; 61 private VisibilityLocationProvider mVisibilityLocationProvider; 62 private ArraySet<View> mAllowedReorderViews = new ArraySet<>(); 63 private ArraySet<NotificationEntry> mLowPriorityReorderingViews = new ArraySet<>(); 64 private ArraySet<View> mAddedChildren = new ArraySet<>(); 65 private boolean mPulsing; 66 67 /** 68 * Injected constructor. See {@link NotificationsModule}. 69 */ VisualStabilityManager( NotificationEntryManager notificationEntryManager, @Main Handler handler, StatusBarStateController statusBarStateController, WakefulnessLifecycle wakefulnessLifecycle)70 public VisualStabilityManager( 71 NotificationEntryManager notificationEntryManager, 72 @Main Handler handler, 73 StatusBarStateController statusBarStateController, 74 WakefulnessLifecycle wakefulnessLifecycle) { 75 76 mHandler = handler; 77 78 if (notificationEntryManager != null) { 79 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { 80 @Override 81 public void onPreEntryUpdated(NotificationEntry entry) { 82 final boolean ambientStateHasChanged = 83 entry.isAmbient() != entry.getRow().isLowPriority(); 84 if (ambientStateHasChanged) { 85 // note: entries are removed in onReorderingFinished 86 mLowPriorityReorderingViews.add(entry); 87 } 88 } 89 }); 90 } 91 92 if (statusBarStateController != null) { 93 setPulsing(statusBarStateController.isPulsing()); 94 statusBarStateController.addCallback(new StatusBarStateController.StateListener() { 95 @Override 96 public void onPulsingChanged(boolean pulsing) { 97 setPulsing(pulsing); 98 } 99 100 @Override 101 public void onExpandedChanged(boolean expanded) { 102 setPanelExpanded(expanded); 103 } 104 }); 105 } 106 107 if (wakefulnessLifecycle != null) { 108 wakefulnessLifecycle.addObserver(mWakefulnessObserver); 109 } 110 } 111 112 /** 113 * Add a callback to invoke when reordering is allowed again. 114 * 115 * @param callback the callback to add 116 * @param persistent {@code true} if this callback should this callback be persisted, otherwise 117 * it will be removed after a single invocation 118 */ addReorderingAllowedCallback(Callback callback, boolean persistent)119 public void addReorderingAllowedCallback(Callback callback, boolean persistent) { 120 if (persistent) { 121 mPersistentReorderingCallbacks.add(callback); 122 } 123 if (mReorderingAllowedCallbacks.contains(callback)) { 124 return; 125 } 126 mReorderingAllowedCallbacks.add(callback); 127 } 128 129 /** 130 * Add a callback to invoke when group changes are allowed again. 131 * 132 * @param callback the callback to add 133 * @param persistent {@code true} if this callback should this callback be persisted, otherwise 134 * it will be removed after a single invocation 135 */ addGroupChangesAllowedCallback(Callback callback, boolean persistent)136 public void addGroupChangesAllowedCallback(Callback callback, boolean persistent) { 137 if (persistent) { 138 mPersistentGroupCallbacks.add(callback); 139 } 140 if (mGroupChangesAllowedCallbacks.contains(callback)) { 141 return; 142 } 143 mGroupChangesAllowedCallbacks.add(callback); 144 } 145 146 /** 147 * @param screenOn whether the screen is on 148 */ setScreenOn(boolean screenOn)149 private void setScreenOn(boolean screenOn) { 150 mScreenOn = screenOn; 151 updateAllowedStates(); 152 } 153 154 /** 155 * Set the panel to be expanded. 156 */ setPanelExpanded(boolean expanded)157 private void setPanelExpanded(boolean expanded) { 158 mPanelExpanded = expanded; 159 updateAllowedStates(); 160 } 161 162 /** 163 * @param pulsing whether we are currently pulsing for ambient display. 164 */ setPulsing(boolean pulsing)165 private void setPulsing(boolean pulsing) { 166 if (mPulsing == pulsing) { 167 return; 168 } 169 mPulsing = pulsing; 170 updateAllowedStates(); 171 } 172 updateAllowedStates()173 private void updateAllowedStates() { 174 boolean reorderingAllowed = 175 (!mScreenOn || !mPanelExpanded || mIsTemporaryReorderingAllowed) && !mPulsing; 176 boolean changedToTrue = reorderingAllowed && !mReorderingAllowed; 177 mReorderingAllowed = reorderingAllowed; 178 if (changedToTrue) { 179 notifyChangeAllowed(mReorderingAllowedCallbacks, mPersistentReorderingCallbacks); 180 } 181 boolean groupChangesAllowed = (!mScreenOn || !mPanelExpanded) && !mPulsing; 182 changedToTrue = groupChangesAllowed && !mGroupChangedAllowed; 183 mGroupChangedAllowed = groupChangesAllowed; 184 if (changedToTrue) { 185 notifyChangeAllowed(mGroupChangesAllowedCallbacks, mPersistentGroupCallbacks); 186 } 187 } 188 notifyChangeAllowed(ArrayList<Callback> callbacks, ArraySet<Callback> persistentCallbacks)189 private void notifyChangeAllowed(ArrayList<Callback> callbacks, 190 ArraySet<Callback> persistentCallbacks) { 191 for (int i = 0; i < callbacks.size(); i++) { 192 Callback callback = callbacks.get(i); 193 callback.onChangeAllowed(); 194 if (!persistentCallbacks.contains(callback)) { 195 callbacks.remove(callback); 196 i--; 197 } 198 } 199 } 200 201 /** 202 * @return whether reordering is currently allowed in general. 203 */ isReorderingAllowed()204 public boolean isReorderingAllowed() { 205 return mReorderingAllowed; 206 } 207 208 /** 209 * @return whether changes in the grouping should be allowed right now. 210 */ areGroupChangesAllowed()211 public boolean areGroupChangesAllowed() { 212 return mGroupChangedAllowed; 213 } 214 215 /** 216 * @return whether a specific notification is allowed to reorder. Certain notifications are 217 * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added 218 * notifications or heads-up notifications that are out of view. 219 */ canReorderNotification(ExpandableNotificationRow row)220 public boolean canReorderNotification(ExpandableNotificationRow row) { 221 if (mReorderingAllowed) { 222 return true; 223 } 224 if (mAddedChildren.contains(row)) { 225 return true; 226 } 227 if (mLowPriorityReorderingViews.contains(row.getEntry())) { 228 return true; 229 } 230 if (mAllowedReorderViews.contains(row) 231 && !mVisibilityLocationProvider.isInVisibleLocation(row.getEntry())) { 232 return true; 233 } 234 return false; 235 } 236 setVisibilityLocationProvider( VisibilityLocationProvider visibilityLocationProvider)237 public void setVisibilityLocationProvider( 238 VisibilityLocationProvider visibilityLocationProvider) { 239 mVisibilityLocationProvider = visibilityLocationProvider; 240 } 241 242 /** 243 * Notifications have been reordered, so reset all the allowed list of views that are allowed 244 * to reorder. 245 */ onReorderingFinished()246 public void onReorderingFinished() { 247 mAllowedReorderViews.clear(); 248 mAddedChildren.clear(); 249 mLowPriorityReorderingViews.clear(); 250 } 251 252 @Override onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)253 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { 254 if (isHeadsUp) { 255 // Heads up notifications should in general be allowed to reorder if they are out of 256 // view and stay at the current location if they aren't. 257 mAllowedReorderViews.add(entry.getRow()); 258 } 259 } 260 261 /** 262 * Temporarily allows reordering of the entire shade for a period of 1000ms. Subsequent calls 263 * to this method will extend the timer. 264 */ temporarilyAllowReordering()265 public void temporarilyAllowReordering() { 266 mHandler.removeCallbacks(mOnTemporaryReorderingExpired); 267 mHandler.postDelayed(mOnTemporaryReorderingExpired, TEMPORARY_REORDERING_ALLOWED_DURATION); 268 if (!mIsTemporaryReorderingAllowed) { 269 mTemporaryReorderingStart = SystemClock.elapsedRealtime(); 270 } 271 mIsTemporaryReorderingAllowed = true; 272 updateAllowedStates(); 273 } 274 275 private final Runnable mOnTemporaryReorderingExpired = () -> { 276 mIsTemporaryReorderingAllowed = false; 277 updateAllowedStates(); 278 }; 279 280 /** 281 * Notify the visual stability manager that a new view was added and should be allowed to 282 * reorder next time. 283 */ notifyViewAddition(View view)284 public void notifyViewAddition(View view) { 285 mAddedChildren.add(view); 286 } 287 288 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)289 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 290 pw.println("VisualStabilityManager state:"); 291 pw.print(" mIsTemporaryReorderingAllowed="); pw.println(mIsTemporaryReorderingAllowed); 292 pw.print(" mTemporaryReorderingStart="); pw.println(mTemporaryReorderingStart); 293 294 long now = SystemClock.elapsedRealtime(); 295 pw.print(" Temporary reordering window has been open for "); 296 pw.print(now - (mIsTemporaryReorderingAllowed ? mTemporaryReorderingStart : now)); 297 pw.println("ms"); 298 299 pw.println(); 300 } 301 302 final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() { 303 @Override 304 public void onFinishedGoingToSleep() { 305 setScreenOn(false); 306 } 307 308 @Override 309 public void onStartedWakingUp() { 310 setScreenOn(true); 311 } 312 }; 313 314 315 /** 316 * See {@link Callback#onChangeAllowed()} 317 */ 318 public interface Callback { 319 320 /** 321 * Called when changing is allowed again. 322 */ onChangeAllowed()323 void onChangeAllowed(); 324 } 325 } 326