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 static com.android.systemui.Dependency.MAIN_HANDLER_NAME; 20 21 import android.os.Handler; 22 import android.os.SystemClock; 23 import android.view.View; 24 25 import androidx.collection.ArraySet; 26 27 import com.android.systemui.Dumpable; 28 import com.android.systemui.statusbar.NotificationPresenter; 29 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 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 import javax.inject.Inject; 38 import javax.inject.Named; 39 import javax.inject.Singleton; 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 @Singleton 46 public class VisualStabilityManager implements OnHeadsUpChangedListener, Dumpable { 47 48 private static final long TEMPORARY_REORDERING_ALLOWED_DURATION = 1000; 49 50 private final ArrayList<Callback> mCallbacks = new ArrayList<>(); 51 private final Handler mHandler; 52 53 private NotificationPresenter mPresenter; 54 private boolean mPanelExpanded; 55 private boolean mScreenOn; 56 private boolean mReorderingAllowed; 57 private boolean mIsTemporaryReorderingAllowed; 58 private long mTemporaryReorderingStart; 59 private VisibilityLocationProvider mVisibilityLocationProvider; 60 private ArraySet<View> mAllowedReorderViews = new ArraySet<>(); 61 private ArraySet<NotificationEntry> mLowPriorityReorderingViews = new ArraySet<>(); 62 private ArraySet<View> mAddedChildren = new ArraySet<>(); 63 private boolean mPulsing; 64 65 @Inject VisualStabilityManager( NotificationEntryManager notificationEntryManager, @Named(MAIN_HANDLER_NAME) Handler handler)66 public VisualStabilityManager( 67 NotificationEntryManager notificationEntryManager, 68 @Named(MAIN_HANDLER_NAME) Handler handler) { 69 70 mHandler = handler; 71 72 notificationEntryManager.addNotificationEntryListener(new NotificationEntryListener() { 73 @Override 74 public void onPreEntryUpdated(NotificationEntry entry) { 75 final boolean mAmbientStateHasChanged = 76 entry.ambient != entry.getRow().isLowPriority(); 77 if (mAmbientStateHasChanged) { 78 mLowPriorityReorderingViews.add(entry); 79 } 80 } 81 82 @Override 83 public void onPostEntryUpdated(NotificationEntry entry) { 84 // This line is technically not required as we'll get called as the hierarchy 85 // manager will call onReorderingFinished() immediately before this. 86 // TODO: Find a way to make this relationship more explicit 87 mLowPriorityReorderingViews.remove(entry); 88 } 89 }); 90 } 91 setUpWithPresenter(NotificationPresenter presenter)92 public void setUpWithPresenter(NotificationPresenter presenter) { 93 mPresenter = presenter; 94 } 95 96 /** 97 * Add a callback to invoke when reordering is allowed again. 98 * @param callback 99 */ addReorderingAllowedCallback(Callback callback)100 public void addReorderingAllowedCallback(Callback callback) { 101 if (mCallbacks.contains(callback)) { 102 return; 103 } 104 mCallbacks.add(callback); 105 } 106 107 /** 108 * Set the panel to be expanded. 109 */ setPanelExpanded(boolean expanded)110 public void setPanelExpanded(boolean expanded) { 111 mPanelExpanded = expanded; 112 updateReorderingAllowed(); 113 } 114 115 /** 116 * @param screenOn whether the screen is on 117 */ setScreenOn(boolean screenOn)118 public void setScreenOn(boolean screenOn) { 119 mScreenOn = screenOn; 120 updateReorderingAllowed(); 121 } 122 123 /** 124 * @param pulsing whether we are currently pulsing for ambient display. 125 */ setPulsing(boolean pulsing)126 public void setPulsing(boolean pulsing) { 127 if (mPulsing == pulsing) { 128 return; 129 } 130 mPulsing = pulsing; 131 updateReorderingAllowed(); 132 } 133 updateReorderingAllowed()134 private void updateReorderingAllowed() { 135 boolean reorderingAllowed = 136 (!mScreenOn || !mPanelExpanded || mIsTemporaryReorderingAllowed) && !mPulsing; 137 boolean changedToTrue = reorderingAllowed && !mReorderingAllowed; 138 mReorderingAllowed = reorderingAllowed; 139 if (changedToTrue) { 140 notifyCallbacks(); 141 } 142 } 143 notifyCallbacks()144 private void notifyCallbacks() { 145 for (int i = 0; i < mCallbacks.size(); i++) { 146 Callback callback = mCallbacks.get(i); 147 callback.onReorderingAllowed(); 148 } 149 mCallbacks.clear(); 150 } 151 152 /** 153 * @return whether reordering is currently allowed in general. 154 */ isReorderingAllowed()155 public boolean isReorderingAllowed() { 156 return mReorderingAllowed; 157 } 158 159 /** 160 * @return whether a specific notification is allowed to reorder. Certain notifications are 161 * allowed to reorder even if {@link #isReorderingAllowed()} returns false, like newly added 162 * notifications or heads-up notifications that are out of view. 163 */ canReorderNotification(ExpandableNotificationRow row)164 public boolean canReorderNotification(ExpandableNotificationRow row) { 165 if (mReorderingAllowed) { 166 return true; 167 } 168 if (mAddedChildren.contains(row)) { 169 return true; 170 } 171 if (mLowPriorityReorderingViews.contains(row.getEntry())) { 172 return true; 173 } 174 if (mAllowedReorderViews.contains(row) 175 && !mVisibilityLocationProvider.isInVisibleLocation(row.getEntry())) { 176 return true; 177 } 178 return false; 179 } 180 setVisibilityLocationProvider( VisibilityLocationProvider visibilityLocationProvider)181 public void setVisibilityLocationProvider( 182 VisibilityLocationProvider visibilityLocationProvider) { 183 mVisibilityLocationProvider = visibilityLocationProvider; 184 } 185 onReorderingFinished()186 public void onReorderingFinished() { 187 mAllowedReorderViews.clear(); 188 mAddedChildren.clear(); 189 mLowPriorityReorderingViews.clear(); 190 } 191 192 @Override onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp)193 public void onHeadsUpStateChanged(NotificationEntry entry, boolean isHeadsUp) { 194 if (isHeadsUp) { 195 // Heads up notifications should in general be allowed to reorder if they are out of 196 // view and stay at the current location if they aren't. 197 mAllowedReorderViews.add(entry.getRow()); 198 } 199 } 200 201 /** 202 * Temporarily allows reordering of the entire shade for a period of 1000ms. Subsequent calls 203 * to this method will extend the timer. 204 */ temporarilyAllowReordering()205 public void temporarilyAllowReordering() { 206 mHandler.removeCallbacks(mOnTemporaryReorderingExpired); 207 mHandler.postDelayed(mOnTemporaryReorderingExpired, TEMPORARY_REORDERING_ALLOWED_DURATION); 208 if (!mIsTemporaryReorderingAllowed) { 209 mTemporaryReorderingStart = SystemClock.elapsedRealtime(); 210 } 211 mIsTemporaryReorderingAllowed = true; 212 updateReorderingAllowed(); 213 } 214 215 private final Runnable mOnTemporaryReorderingExpired = () -> { 216 mIsTemporaryReorderingAllowed = false; 217 updateReorderingAllowed(); 218 }; 219 220 /** 221 * Notify the visual stability manager that a new view was added and should be allowed to 222 * reorder next time. 223 */ notifyViewAddition(View view)224 public void notifyViewAddition(View view) { 225 mAddedChildren.add(view); 226 } 227 228 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)229 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 230 pw.println("VisualStabilityManager state:"); 231 pw.print(" mIsTemporaryReorderingAllowed="); pw.println(mIsTemporaryReorderingAllowed); 232 pw.print(" mTemporaryReorderingStart="); pw.println(mTemporaryReorderingStart); 233 234 long now = SystemClock.elapsedRealtime(); 235 pw.print(" Temporary reordering window has been open for "); 236 pw.print(now - (mIsTemporaryReorderingAllowed ? mTemporaryReorderingStart : now)); 237 pw.println("ms"); 238 239 pw.println(); 240 } 241 242 public interface Callback { 243 /** 244 * Called when reordering is allowed again. 245 */ onReorderingAllowed()246 void onReorderingAllowed(); 247 } 248 249 } 250