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