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