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