1 /* 2 * Copyright (C) 2019 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.animation.ObjectAnimator 20 import android.util.FloatProperty 21 import com.android.systemui.animation.Interpolators 22 import com.android.systemui.dagger.SysUISingleton 23 import com.android.systemui.plugins.statusbar.StatusBarStateController 24 import com.android.systemui.statusbar.StatusBarState 25 import com.android.systemui.statusbar.notification.collection.NotificationEntry 26 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 27 import com.android.systemui.statusbar.notification.stack.StackStateAnimator 28 import com.android.systemui.statusbar.phone.DozeParameters 29 import com.android.systemui.statusbar.phone.KeyguardBypassController 30 import com.android.systemui.statusbar.phone.PanelExpansionListener 31 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController 32 import com.android.systemui.statusbar.policy.HeadsUpManager 33 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener 34 import javax.inject.Inject 35 import kotlin.math.min 36 37 @SysUISingleton 38 class NotificationWakeUpCoordinator @Inject constructor( 39 private val mHeadsUpManager: HeadsUpManager, 40 private val statusBarStateController: StatusBarStateController, 41 private val bypassController: KeyguardBypassController, 42 private val dozeParameters: DozeParameters, 43 private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController 44 ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener { 45 46 private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>( 47 "notificationVisibility") { 48 setValuenull49 override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) { 50 coordinator.setVisibilityAmount(value) 51 } 52 getnull53 override fun get(coordinator: NotificationWakeUpCoordinator): Float? { 54 return coordinator.mLinearVisibilityAmount 55 } 56 } 57 private lateinit var mStackScrollerController: NotificationStackScrollLayoutController 58 private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE 59 60 private var mLinearDozeAmount: Float = 0.0f 61 private var mDozeAmount: Float = 0.0f 62 private var mNotificationVisibleAmount = 0.0f 63 private var mNotificationsVisible = false 64 private var mNotificationsVisibleForExpansion = false 65 private var mVisibilityAnimator: ObjectAnimator? = null 66 private var mVisibilityAmount = 0.0f 67 private var mLinearVisibilityAmount = 0.0f 68 private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>() 69 private var pulseExpanding: Boolean = false 70 private val wakeUpListeners = arrayListOf<WakeUpListener>() 71 private var state: Int = StatusBarState.KEYGUARD 72 73 var fullyAwake: Boolean = false 74 75 var wakingUp = false 76 set(value) { 77 field = value 78 willWakeUp = false 79 if (value) { 80 if (mNotificationsVisible && !mNotificationsVisibleForExpansion && 81 !bypassController.bypassEnabled) { 82 // We're waking up while pulsing, let's make sure the animation looks nice 83 mStackScrollerController.wakeUpFromPulse() 84 } 85 if (bypassController.bypassEnabled && !mNotificationsVisible) { 86 // Let's make sure our huns become visible once we are waking up in case 87 // they were blocked by the proximity sensor 88 updateNotificationVisibility(animate = shouldAnimateVisibility(), 89 increaseSpeed = false) 90 } 91 } 92 } 93 94 var willWakeUp = false 95 set(value) { 96 if (!value || mDozeAmount != 0.0f) { 97 field = value 98 } 99 } 100 101 private var collapsedEnoughToHide: Boolean = false 102 103 var pulsing: Boolean = false 104 set(value) { 105 field = value 106 if (value) { 107 // Only when setting pulsing to true we want an immediate update, since we get 108 // this already when the doze service finishes which is usually before we get 109 // the waking up callback 110 updateNotificationVisibility(animate = shouldAnimateVisibility(), 111 increaseSpeed = false) 112 } 113 } 114 115 var notificationsFullyHidden: Boolean = false 116 private set(value) { 117 if (field != value) { 118 field = value 119 for (listener in wakeUpListeners) { 120 listener.onFullyHiddenChanged(value) 121 } 122 } 123 } 124 /** 125 * True if we can show pulsing heads up notifications 126 */ 127 var canShowPulsingHuns: Boolean = false 128 private set 129 get() { 130 var canShow = pulsing 131 if (bypassController.bypassEnabled) { 132 // We also allow pulsing on the lock screen! 133 canShow = canShow || (wakingUp || willWakeUp || fullyAwake) && 134 statusBarStateController.state == StatusBarState.KEYGUARD 135 // We want to hide the notifications when collapsed too much 136 if (collapsedEnoughToHide) { 137 canShow = false 138 } 139 } 140 return canShow 141 } 142 143 init { 144 mHeadsUpManager.addListener(this) 145 statusBarStateController.addCallback(this) 146 addListener(object : WakeUpListener { onFullyHiddenChangednull147 override fun onFullyHiddenChanged(isFullyHidden: Boolean) { 148 if (isFullyHidden && mNotificationsVisibleForExpansion) { 149 // When the notification becomes fully invisible, let's make sure our expansion 150 // flag also changes. This can happen if the bouncer shows when dragging down 151 // and then the screen turning off, where we don't reset this state. 152 setNotificationsVisibleForExpansion(visible = false, animate = false, 153 increaseSpeed = false) 154 } 155 } 156 }) 157 } 158 setStackScrollernull159 fun setStackScroller(stackScrollerController: NotificationStackScrollLayoutController) { 160 mStackScrollerController = stackScrollerController 161 pulseExpanding = stackScrollerController.isPulseExpanding 162 stackScrollerController.setOnPulseHeightChangedListener { 163 val nowExpanding = isPulseExpanding() 164 val changed = nowExpanding != pulseExpanding 165 pulseExpanding = nowExpanding 166 for (listener in wakeUpListeners) { 167 listener.onPulseExpansionChanged(changed) 168 } 169 } 170 } 171 isPulseExpandingnull172 fun isPulseExpanding(): Boolean = mStackScrollerController.isPulseExpanding 173 174 /** 175 * @param visible should notifications be visible 176 * @param animate should this change be animated 177 * @param increaseSpeed should the speed be increased of the animation 178 */ 179 fun setNotificationsVisibleForExpansion( 180 visible: Boolean, 181 animate: Boolean, 182 increaseSpeed: Boolean 183 ) { 184 mNotificationsVisibleForExpansion = visible 185 updateNotificationVisibility(animate, increaseSpeed) 186 if (!visible && mNotificationsVisible) { 187 // If we stopped expanding and we're still visible because we had a pulse that hasn't 188 // times out, let's release them all to make sure were not stuck in a state where 189 // notifications are visible 190 mHeadsUpManager.releaseAllImmediately() 191 } 192 } 193 addListenernull194 fun addListener(listener: WakeUpListener) { 195 wakeUpListeners.add(listener) 196 } 197 removeListenernull198 fun removeListener(listener: WakeUpListener) { 199 wakeUpListeners.remove(listener) 200 } 201 updateNotificationVisibilitynull202 private fun updateNotificationVisibility( 203 animate: Boolean, 204 increaseSpeed: Boolean 205 ) { 206 // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore 207 var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications() 208 visible = visible && canShowPulsingHuns 209 210 if (!visible && mNotificationsVisible && (wakingUp || willWakeUp) && mDozeAmount != 0.0f) { 211 // let's not make notifications invisible while waking up, otherwise the animation 212 // is strange 213 return 214 } 215 setNotificationsVisible(visible, animate, increaseSpeed) 216 } 217 setNotificationsVisiblenull218 private fun setNotificationsVisible( 219 visible: Boolean, 220 animate: Boolean, 221 increaseSpeed: Boolean 222 ) { 223 if (mNotificationsVisible == visible) { 224 return 225 } 226 mNotificationsVisible = visible 227 mVisibilityAnimator?.cancel() 228 if (animate) { 229 notifyAnimationStart(visible) 230 startVisibilityAnimation(increaseSpeed) 231 } else { 232 setVisibilityAmount(if (visible) 1.0f else 0.0f) 233 } 234 } 235 onDozeAmountChangednull236 override fun onDozeAmountChanged(linear: Float, eased: Float) { 237 if (overrideDozeAmountIfAnimatingScreenOff(linear)) { 238 return 239 } 240 241 if (overrideDozeAmountIfBypass()) { 242 return 243 } 244 245 if (linear != 1.0f && linear != 0.0f && 246 (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) { 247 // Let's notify the scroller that an animation started 248 notifyAnimationStart(mLinearDozeAmount == 1.0f) 249 } 250 setDozeAmount(linear, eased) 251 } 252 setDozeAmountnull253 fun setDozeAmount(linear: Float, eased: Float) { 254 val changed = linear != mLinearDozeAmount 255 mLinearDozeAmount = linear 256 mDozeAmount = eased 257 mStackScrollerController.setDozeAmount(mDozeAmount) 258 updateHideAmount() 259 if (changed && linear == 0.0f) { 260 setNotificationsVisible(visible = false, animate = false, increaseSpeed = false) 261 setNotificationsVisibleForExpansion(visible = false, animate = false, 262 increaseSpeed = false) 263 } 264 } 265 onStateChangednull266 override fun onStateChanged(newState: Int) { 267 if (dozeParameters.shouldControlUnlockedScreenOff()) { 268 if (unlockedScreenOffAnimationController.isScreenOffAnimationPlaying() && 269 state == StatusBarState.KEYGUARD && 270 newState == StatusBarState.SHADE) { 271 // If we're animating the screen off and going from KEYGUARD back to SHADE, the 272 // animation was cancelled and we are unlocking. Override the doze amount to 0f (not 273 // dozing) so that the notifications are no longer hidden. 274 setDozeAmount(0f, 0f) 275 } 276 } 277 278 if (overrideDozeAmountIfAnimatingScreenOff(mLinearDozeAmount)) { 279 return 280 } 281 282 if (overrideDozeAmountIfBypass()) { 283 return 284 } 285 286 if (bypassController.bypassEnabled && 287 newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED && 288 (!statusBarStateController.isDozing || shouldAnimateVisibility())) { 289 // We're leaving shade locked. Let's animate the notifications away 290 setNotificationsVisible(visible = true, increaseSpeed = false, animate = false) 291 setNotificationsVisible(visible = false, increaseSpeed = false, animate = true) 292 } 293 294 this.state = newState 295 } 296 onPanelExpansionChangednull297 override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) { 298 val collapsedEnough = expansion <= 0.9f 299 if (collapsedEnough != this.collapsedEnoughToHide) { 300 val couldShowPulsingHuns = canShowPulsingHuns 301 this.collapsedEnoughToHide = collapsedEnough 302 if (couldShowPulsingHuns && !canShowPulsingHuns) { 303 updateNotificationVisibility(animate = true, increaseSpeed = true) 304 mHeadsUpManager.releaseAllImmediately() 305 } 306 } 307 } 308 309 /** 310 * @return Whether the doze amount was overridden because bypass is enabled. If true, the 311 * original doze amount should be ignored. 312 */ overrideDozeAmountIfBypassnull313 private fun overrideDozeAmountIfBypass(): Boolean { 314 if (bypassController.bypassEnabled) { 315 var amount = 1.0f 316 if (statusBarStateController.state == StatusBarState.SHADE || 317 statusBarStateController.state == StatusBarState.SHADE_LOCKED) { 318 amount = 0.0f 319 } 320 setDozeAmount(amount, amount) 321 return true 322 } 323 return false 324 } 325 326 /** 327 * If we're playing the screen off animation, force the notification doze amount to be 1f (fully 328 * dozing). This is needed so that the notifications aren't briefly visible as the screen turns 329 * off and dozeAmount goes from 1f to 0f. 330 * 331 * @return Whether the doze amount was overridden because we are playing the screen off 332 * animation. If true, the original doze amount should be ignored. 333 */ overrideDozeAmountIfAnimatingScreenOffnull334 private fun overrideDozeAmountIfAnimatingScreenOff(linearDozeAmount: Float): Boolean { 335 if (unlockedScreenOffAnimationController.isScreenOffAnimationPlaying()) { 336 setDozeAmount(1f, 1f) 337 return true 338 } 339 340 return false 341 } 342 startVisibilityAnimationnull343 private fun startVisibilityAnimation(increaseSpeed: Boolean) { 344 if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) { 345 mVisibilityInterpolator = if (mNotificationsVisible) 346 Interpolators.TOUCH_RESPONSE 347 else 348 Interpolators.FAST_OUT_SLOW_IN_REVERSE 349 } 350 val target = if (mNotificationsVisible) 1.0f else 0.0f 351 val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target) 352 visibilityAnimator.setInterpolator(Interpolators.LINEAR) 353 var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong() 354 if (increaseSpeed) { 355 duration = (duration.toFloat() / 1.5F).toLong() 356 } 357 visibilityAnimator.setDuration(duration) 358 visibilityAnimator.start() 359 mVisibilityAnimator = visibilityAnimator 360 } 361 setVisibilityAmountnull362 private fun setVisibilityAmount(visibilityAmount: Float) { 363 mLinearVisibilityAmount = visibilityAmount 364 mVisibilityAmount = mVisibilityInterpolator.getInterpolation( 365 visibilityAmount) 366 handleAnimationFinished() 367 updateHideAmount() 368 } 369 handleAnimationFinishednull370 private fun handleAnimationFinished() { 371 if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) { 372 mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) } 373 mEntrySetToClearWhenFinished.clear() 374 } 375 } 376 updateHideAmountnull377 private fun updateHideAmount() { 378 val linearAmount = min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount) 379 val amount = min(1.0f - mVisibilityAmount, mDozeAmount) 380 mStackScrollerController.setHideAmount(linearAmount, amount) 381 notificationsFullyHidden = linearAmount == 1.0f 382 } 383 notifyAnimationStartnull384 private fun notifyAnimationStart(awake: Boolean) { 385 mStackScrollerController.notifyHideAnimationStart(!awake) 386 } 387 onDozingChangednull388 override fun onDozingChanged(isDozing: Boolean) { 389 if (isDozing) { 390 setNotificationsVisible(visible = false, animate = false, increaseSpeed = false) 391 } 392 } 393 onHeadsUpStateChangednull394 override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) { 395 var animate = shouldAnimateVisibility() 396 if (!isHeadsUp) { 397 if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) { 398 if (entry.isRowDismissed) { 399 // if we animate, we see the shelf briefly visible. Instead we fully animate 400 // the notification and its background out 401 animate = false 402 } else if (!wakingUp && !willWakeUp) { 403 // TODO: look that this is done properly and not by anyone else 404 entry.setHeadsUpAnimatingAway(true) 405 mEntrySetToClearWhenFinished.add(entry) 406 } 407 } 408 } else if (mEntrySetToClearWhenFinished.contains(entry)) { 409 mEntrySetToClearWhenFinished.remove(entry) 410 entry.setHeadsUpAnimatingAway(false) 411 } 412 updateNotificationVisibility(animate, increaseSpeed = false) 413 } 414 shouldAnimateVisibilitynull415 private fun shouldAnimateVisibility() = 416 dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking 417 418 interface WakeUpListener { 419 /** 420 * Called whenever the notifications are fully hidden or shown 421 */ 422 @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {} 423 424 /** 425 * Called whenever the pulseExpansion changes 426 * @param expandingChanged if the user has started or stopped expanding 427 */ 428 @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {} 429 } 430 }