• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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