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