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