1 /*
2  * Copyright (C) 2020 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.coordinator;
18 
19 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
20 
21 import android.util.Log;
22 
23 import androidx.annotation.NonNull;
24 import androidx.annotation.VisibleForTesting;
25 
26 import com.android.systemui.Dumpable;
27 import com.android.systemui.dagger.SysUISingleton;
28 import com.android.systemui.dump.DumpManager;
29 import com.android.systemui.keyguard.WakefulnessLifecycle;
30 import com.android.systemui.plugins.statusbar.StatusBarStateController;
31 import com.android.systemui.shade.ShadeStateEvents;
32 import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
33 import com.android.systemui.statusbar.notification.collection.GroupEntry;
34 import com.android.systemui.statusbar.notification.collection.ListEntry;
35 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
36 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
37 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
38 import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
39 import com.android.systemui.statusbar.policy.HeadsUpManager;
40 import com.android.systemui.util.Compile;
41 import com.android.systemui.util.concurrency.DelayableExecutor;
42 
43 import java.io.PrintWriter;
44 import java.util.HashMap;
45 import java.util.HashSet;
46 import java.util.Map;
47 import java.util.Set;
48 
49 import javax.inject.Inject;
50 
51 /**
52  * Ensures that notifications are visually stable if the user is looking at the notifications.
53  * Group and section changes are re-allowed when the notification entries are no longer being
54  * viewed.
55  */
56 // TODO(b/204468557): Move to @CoordinatorScope
57 @SysUISingleton
58 public class VisualStabilityCoordinator implements Coordinator, Dumpable,
59         ShadeStateEvents.ShadeStateEventsListener {
60     public static final String TAG = "VisualStability";
61     public static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
62     private final DelayableExecutor mDelayableExecutor;
63     private final HeadsUpManager mHeadsUpManager;
64     private final ShadeStateEvents mShadeStateEvents;
65     private final StatusBarStateController mStatusBarStateController;
66     private final VisibilityLocationProvider mVisibilityLocationProvider;
67     private final VisualStabilityProvider mVisualStabilityProvider;
68     private final WakefulnessLifecycle mWakefulnessLifecycle;
69 
70     private boolean mSleepy = true;
71     private boolean mFullyDozed;
72     private boolean mPanelExpanded;
73     private boolean mPulsing;
74     private boolean mNotifPanelCollapsing;
75     private boolean mNotifPanelLaunchingActivity;
76 
77     private boolean mPipelineRunAllowed;
78     private boolean mReorderingAllowed;
79     private boolean mIsSuppressingPipelineRun = false;
80     private boolean mIsSuppressingGroupChange = false;
81     private final Set<String> mEntriesWithSuppressedSectionChange = new HashSet<>();
82     private boolean mIsSuppressingEntryReorder = false;
83 
84     // key: notification key that can temporarily change its section
85     // value: runnable that when run removes its associated RemoveOverrideSuppressionRunnable
86     // from the DelayableExecutor's queue
87     private Map<String, Runnable> mEntriesThatCanChangeSection = new HashMap<>();
88 
89     @VisibleForTesting
90     protected static final long ALLOW_SECTION_CHANGE_TIMEOUT = 500;
91 
92     @Inject
VisualStabilityCoordinator( DelayableExecutor delayableExecutor, DumpManager dumpManager, HeadsUpManager headsUpManager, ShadeStateEvents shadeStateEvents, StatusBarStateController statusBarStateController, VisibilityLocationProvider visibilityLocationProvider, VisualStabilityProvider visualStabilityProvider, WakefulnessLifecycle wakefulnessLifecycle)93     public VisualStabilityCoordinator(
94             DelayableExecutor delayableExecutor,
95             DumpManager dumpManager,
96             HeadsUpManager headsUpManager,
97             ShadeStateEvents shadeStateEvents,
98             StatusBarStateController statusBarStateController,
99             VisibilityLocationProvider visibilityLocationProvider,
100             VisualStabilityProvider visualStabilityProvider,
101             WakefulnessLifecycle wakefulnessLifecycle) {
102         mHeadsUpManager = headsUpManager;
103         mVisibilityLocationProvider = visibilityLocationProvider;
104         mVisualStabilityProvider = visualStabilityProvider;
105         mWakefulnessLifecycle = wakefulnessLifecycle;
106         mStatusBarStateController = statusBarStateController;
107         mDelayableExecutor = delayableExecutor;
108         mShadeStateEvents = shadeStateEvents;
109 
110         dumpManager.registerDumpable(this);
111     }
112 
113     @Override
attach(NotifPipeline pipeline)114     public void attach(NotifPipeline pipeline) {
115         mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
116         mSleepy = mWakefulnessLifecycle.getWakefulness() == WAKEFULNESS_ASLEEP;
117         mFullyDozed = mStatusBarStateController.getDozeAmount() == 1f;
118 
119         mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
120         mPulsing = mStatusBarStateController.isPulsing();
121         mShadeStateEvents.addShadeStateEventsListener(this);
122 
123         pipeline.setVisualStabilityManager(mNotifStabilityManager);
124     }
125 
126     // TODO(b/203826051): Ensure stability manager can allow reordering off-screen
127     //  HUNs to the top of the shade
128     private final NotifStabilityManager mNotifStabilityManager =
129             new NotifStabilityManager("VisualStabilityCoordinator") {
130                 private boolean canMoveForHeadsUp(NotificationEntry entry) {
131                     return entry != null && mHeadsUpManager.isAlerting(entry.getKey())
132                             && !mVisibilityLocationProvider.isInVisibleLocation(entry);
133                 }
134 
135                 @Override
136                 public void onBeginRun() {
137                     mIsSuppressingPipelineRun = false;
138                     mIsSuppressingGroupChange = false;
139                     mEntriesWithSuppressedSectionChange.clear();
140                     mIsSuppressingEntryReorder = false;
141                 }
142 
143                 @Override
144                 public boolean isPipelineRunAllowed() {
145                     mIsSuppressingPipelineRun |= !mPipelineRunAllowed;
146                     return mPipelineRunAllowed;
147                 }
148 
149                 @Override
150                 public boolean isGroupChangeAllowed(@NonNull NotificationEntry entry) {
151                     final boolean isGroupChangeAllowedForEntry =
152                             mReorderingAllowed || canMoveForHeadsUp(entry);
153                     mIsSuppressingGroupChange |= !isGroupChangeAllowedForEntry;
154                     return isGroupChangeAllowedForEntry;
155                 }
156 
157                 @Override
158                 public boolean isGroupPruneAllowed(@NonNull GroupEntry entry) {
159                     final boolean isGroupPruneAllowedForEntry = mReorderingAllowed;
160                     mIsSuppressingGroupChange |= !isGroupPruneAllowedForEntry;
161                     return isGroupPruneAllowedForEntry;
162                 }
163 
164                 @Override
165                 public boolean isSectionChangeAllowed(@NonNull NotificationEntry entry) {
166                     final boolean isSectionChangeAllowedForEntry =
167                             mReorderingAllowed
168                                     || canMoveForHeadsUp(entry)
169                                     || mEntriesThatCanChangeSection.containsKey(entry.getKey());
170                     if (!isSectionChangeAllowedForEntry) {
171                         mEntriesWithSuppressedSectionChange.add(entry.getKey());
172                     }
173                     return isSectionChangeAllowedForEntry;
174                 }
175 
176                 @Override
177                 public boolean isEntryReorderingAllowed(@NonNull ListEntry entry) {
178                     return mReorderingAllowed || canMoveForHeadsUp(entry.getRepresentativeEntry());
179                 }
180 
181                 @Override
182                 public boolean isEveryChangeAllowed() {
183                     return mReorderingAllowed;
184                 }
185 
186                 @Override
187                 public void onEntryReorderSuppressed() {
188                     mIsSuppressingEntryReorder = true;
189                 }
190             };
191 
updateAllowedStates(String field, boolean value)192     private void updateAllowedStates(String field, boolean value) {
193         boolean wasPipelineRunAllowed = mPipelineRunAllowed;
194         boolean wasReorderingAllowed = mReorderingAllowed;
195         mPipelineRunAllowed = !isPanelCollapsingOrLaunchingActivity();
196         mReorderingAllowed = isReorderingAllowed();
197         if (DEBUG && (wasPipelineRunAllowed != mPipelineRunAllowed
198                 || wasReorderingAllowed != mReorderingAllowed)) {
199             Log.d(TAG, "Stability allowances changed:"
200                     + "  pipelineRunAllowed " + wasPipelineRunAllowed + "->" + mPipelineRunAllowed
201                     + "  reorderingAllowed " + wasReorderingAllowed + "->" + mReorderingAllowed
202                     + "  when setting " + field + "=" + value);
203         }
204         if (mPipelineRunAllowed && mIsSuppressingPipelineRun) {
205             mNotifStabilityManager.invalidateList("pipeline run suppression ended");
206         } else if (mReorderingAllowed && (mIsSuppressingGroupChange
207                 || isSuppressingSectionChange()
208                 || mIsSuppressingEntryReorder)) {
209             String reason = "reorder suppression ended for"
210                     + " group=" + mIsSuppressingGroupChange
211                     + " section=" + isSuppressingSectionChange()
212                     + " sort=" + mIsSuppressingEntryReorder;
213             mNotifStabilityManager.invalidateList(reason);
214         }
215         mVisualStabilityProvider.setReorderingAllowed(mReorderingAllowed);
216     }
217 
isSuppressingSectionChange()218     private boolean isSuppressingSectionChange() {
219         return !mEntriesWithSuppressedSectionChange.isEmpty();
220     }
221 
isPanelCollapsingOrLaunchingActivity()222     private boolean isPanelCollapsingOrLaunchingActivity() {
223         return mNotifPanelCollapsing || mNotifPanelLaunchingActivity;
224     }
225 
isReorderingAllowed()226     private boolean isReorderingAllowed() {
227         return ((mFullyDozed && mSleepy) || !mPanelExpanded) && !mPulsing;
228     }
229 
230     /**
231      * Allows this notification entry to be re-ordered in the notification list temporarily until
232      * the timeout has passed.
233      *
234      * Typically this is allowed because the user has directly changed something about the
235      * notification and we are reordering based on the user's change.
236      *
237      * @param entry notification entry that can change sections even if isReorderingAllowed is false
238      * @param now current time SystemClock.uptimeMillis
239      */
temporarilyAllowSectionChanges(@onNull NotificationEntry entry, long now)240     public void temporarilyAllowSectionChanges(@NonNull NotificationEntry entry, long now) {
241         final String entryKey = entry.getKey();
242         final boolean wasSectionChangeAllowed =
243                 mNotifStabilityManager.isSectionChangeAllowed(entry);
244 
245         // If it exists, cancel previous timeout
246         if (mEntriesThatCanChangeSection.containsKey(entryKey)) {
247             mEntriesThatCanChangeSection.get(entryKey).run();
248         }
249 
250         // Schedule & store new timeout cancellable
251         mEntriesThatCanChangeSection.put(
252                 entryKey,
253                 mDelayableExecutor.executeAtTime(
254                         () -> mEntriesThatCanChangeSection.remove(entryKey),
255                         now + ALLOW_SECTION_CHANGE_TIMEOUT));
256 
257         if (!wasSectionChangeAllowed) {
258             mNotifStabilityManager.invalidateList("temporarilyAllowSectionChanges");
259         }
260     }
261 
262     final StatusBarStateController.StateListener mStatusBarStateControllerListener =
263             new StatusBarStateController.StateListener() {
264                 @Override
265                 public void onPulsingChanged(boolean pulsing) {
266                     mPulsing = pulsing;
267                     updateAllowedStates("pulsing", pulsing);
268                 }
269 
270                 @Override
271                 public void onExpandedChanged(boolean expanded) {
272                     mPanelExpanded = expanded;
273                     updateAllowedStates("panelExpanded", expanded);
274                 }
275 
276                 @Override
277                 public void onDozeAmountChanged(float linear, float eased) {
278                     final boolean fullyDozed = linear == 1f;
279                     mFullyDozed = fullyDozed;
280                     updateAllowedStates("fullyDozed", fullyDozed);
281                 }
282             };
283 
284     final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
285         @Override
286         public void onFinishedGoingToSleep() {
287             // NOTE: this method is called much earlier than what we consider "finished" going to
288             // sleep (the animation isn't done), so we also need to check the doze amount is not 1
289             // and use the combo to determine that the locked shade is not visible.
290             mSleepy = true;
291             updateAllowedStates("sleepy", true);
292         }
293 
294         @Override
295         public void onStartedWakingUp() {
296             mSleepy = false;
297             updateAllowedStates("sleepy", false);
298         }
299     };
300 
301     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)302     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
303         pw.println("pipelineRunAllowed: " + mPipelineRunAllowed);
304         pw.println("  notifPanelCollapsing: " + mNotifPanelCollapsing);
305         pw.println("  launchingNotifActivity: " + mNotifPanelLaunchingActivity);
306         pw.println("reorderingAllowed: " + mReorderingAllowed);
307         pw.println("  sleepy: " + mSleepy);
308         pw.println("  fullyDozed: " + mFullyDozed);
309         pw.println("  panelExpanded: " + mPanelExpanded);
310         pw.println("  pulsing: " + mPulsing);
311         pw.println("isSuppressingPipelineRun: " + mIsSuppressingPipelineRun);
312         pw.println("isSuppressingGroupChange: " + mIsSuppressingGroupChange);
313         pw.println("isSuppressingEntryReorder: " + mIsSuppressingEntryReorder);
314         pw.println("entriesWithSuppressedSectionChange: "
315                 + mEntriesWithSuppressedSectionChange.size());
316         for (String key : mEntriesWithSuppressedSectionChange) {
317             pw.println("  " + key);
318         }
319         pw.println("entriesThatCanChangeSection: " + mEntriesThatCanChangeSection.size());
320         for (String key : mEntriesThatCanChangeSection.keySet()) {
321             pw.println("  " + key);
322         }
323     }
324 
325     @Override
onPanelCollapsingChanged(boolean isCollapsing)326     public void onPanelCollapsingChanged(boolean isCollapsing) {
327         mNotifPanelCollapsing = isCollapsing;
328         updateAllowedStates("notifPanelCollapsing", isCollapsing);
329     }
330 
331     @Override
onLaunchingActivityChanged(boolean isLaunchingActivity)332     public void onLaunchingActivityChanged(boolean isLaunchingActivity) {
333         mNotifPanelLaunchingActivity = isLaunchingActivity;
334         updateAllowedStates("notifPanelLaunchingActivity", isLaunchingActivity);
335     }
336 }
337