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