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.util.FloatProperty
20 import android.view.animation.Interpolator
21 import androidx.annotation.VisibleForTesting
22 import androidx.core.animation.ObjectAnimator
23 import com.android.systemui.Dumpable
24 import com.android.app.animation.Interpolators
25 import com.android.app.animation.InterpolatorsAndroidX
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dump.DumpManager
28 import com.android.systemui.plugins.statusbar.StatusBarStateController
29 import com.android.systemui.shade.ShadeExpansionChangeEvent
30 import com.android.systemui.shade.ShadeExpansionListener
31 import com.android.systemui.shade.ShadeViewController
32 import com.android.systemui.statusbar.StatusBarState
33 import com.android.systemui.statusbar.notification.collection.NotificationEntry
34 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
35 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
36 import com.android.systemui.statusbar.phone.DozeParameters
37 import com.android.systemui.statusbar.phone.KeyguardBypassController
38 import com.android.systemui.statusbar.phone.KeyguardBypassController.OnBypassStateChangedListener
39 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
40 import com.android.systemui.statusbar.policy.HeadsUpManager
41 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
42 import com.android.systemui.util.doOnEnd
43 import com.android.systemui.util.doOnStart
44 import java.io.PrintWriter
45 import javax.inject.Inject
46 import kotlin.math.max
47 import kotlin.math.min
48 
49 @SysUISingleton
50 class NotificationWakeUpCoordinator
51 @Inject
52 constructor(
53     dumpManager: DumpManager,
54     private val mHeadsUpManager: HeadsUpManager,
55     private val statusBarStateController: StatusBarStateController,
56     private val bypassController: KeyguardBypassController,
57     private val dozeParameters: DozeParameters,
58     private val screenOffAnimationController: ScreenOffAnimationController,
59     private val logger: NotificationWakeUpCoordinatorLogger,
60 ) :
61     OnHeadsUpChangedListener,
62     StatusBarStateController.StateListener,
63     ShadeExpansionListener,
64     Dumpable {
65     private lateinit var mStackScrollerController: NotificationStackScrollLayoutController
66     private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
67 
68     private var inputLinearDozeAmount: Float = 0.0f
69     private var inputEasedDozeAmount: Float = 0.0f
70     private var delayedDozeAmountOverride: Float = 0.0f
71     private var delayedDozeAmountAnimator: ObjectAnimator? = null
72     /** Valid values: {1f, 0f, null} null => use input */
73     private var hardDozeAmountOverride: Float? = null
74     private var hardDozeAmountOverrideSource: String = "n/a"
75     private var outputLinearDozeAmount: Float = 0.0f
76     private var outputEasedDozeAmount: Float = 0.0f
77     @VisibleForTesting val dozeAmountInterpolator: Interpolator = Interpolators.FAST_OUT_SLOW_IN
78 
79     private var mNotificationVisibleAmount = 0.0f
80     private var mNotificationsVisible = false
81     private var mNotificationsVisibleForExpansion = false
82     private var mVisibilityAnimator: ObjectAnimator? = null
83     private var mVisibilityAmount = 0.0f
84     private var mLinearVisibilityAmount = 0.0f
85     private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
86     private var pulseExpanding: Boolean = false
87     private val wakeUpListeners = arrayListOf<WakeUpListener>()
88     private var state: Int = StatusBarState.KEYGUARD
89 
90     var fullyAwake: Boolean = false
91 
92     var wakingUp = false
93         private set(value) {
94             field = value
95             willWakeUp = false
96             if (value) {
97                 if (
98                     mNotificationsVisible &&
99                         !mNotificationsVisibleForExpansion &&
100                         !bypassController.bypassEnabled
101                 ) {
102                     // We're waking up while pulsing, let's make sure the animation looks nice
103                     mStackScrollerController.wakeUpFromPulse()
104                 }
105                 if (bypassController.bypassEnabled && !mNotificationsVisible) {
106                     // Let's make sure our huns become visible once we are waking up in case
107                     // they were blocked by the proximity sensor
108                     updateNotificationVisibility(
109                         animate = shouldAnimateVisibility(),
110                         increaseSpeed = false
111                     )
112                 }
113             }
114         }
115 
116     var willWakeUp = false
117         set(value) {
118             if (!value || outputLinearDozeAmount != 0.0f) {
119                 field = value
120             }
121         }
122 
123     private var collapsedEnoughToHide: Boolean = false
124 
125     var pulsing: Boolean = false
126         set(value) {
127             field = value
128             if (value) {
129                 // Only when setting pulsing to true we want an immediate update, since we get
130                 // this already when the doze service finishes which is usually before we get
131                 // the waking up callback
132                 updateNotificationVisibility(
133                     animate = shouldAnimateVisibility(),
134                     increaseSpeed = false
135                 )
136             }
137         }
138 
139     var notificationsFullyHidden: Boolean = false
140         private set(value) {
141             if (field != value) {
142                 field = value
143                 for (listener in wakeUpListeners) {
144                     listener.onFullyHiddenChanged(value)
145                 }
146             }
147         }
148 
149     /** True if we can show pulsing heads up notifications */
150     var canShowPulsingHuns: Boolean = false
151         private set
152         get() {
153             var canShow = pulsing
154             if (bypassController.bypassEnabled) {
155                 // We also allow pulsing on the lock screen!
156                 canShow =
157                     canShow ||
158                         (wakingUp || willWakeUp || fullyAwake) &&
159                             statusBarStateController.state == StatusBarState.KEYGUARD
160                 // We want to hide the notifications when collapsed too much
161                 if (collapsedEnoughToHide) {
162                     canShow = false
163                 }
164             }
165             return canShow
166         }
167 
168     private val bypassStateChangedListener =
169         object : OnBypassStateChangedListener {
170             override fun onBypassStateChanged(isEnabled: Boolean) {
171                 // When the bypass state changes, we have to check whether we should re-show the
172                 // notifications by clearing the doze amount override which hides them.
173                 maybeClearHardDozeAmountOverrideHidingNotifs()
174             }
175         }
176 
177     init {
178         dumpManager.registerDumpable(this)
179         mHeadsUpManager.addListener(this)
180         statusBarStateController.addCallback(this)
181         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
182         addListener(
183             object : WakeUpListener {
184                 override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
185                     if (isFullyHidden && mNotificationsVisibleForExpansion) {
186                         // When the notification becomes fully invisible, let's make sure our
187                         // expansion
188                         // flag also changes. This can happen if the bouncer shows when dragging
189                         // down
190                         // and then the screen turning off, where we don't reset this state.
191                         setNotificationsVisibleForExpansion(
192                             visible = false,
193                             animate = false,
194                             increaseSpeed = false
195                         )
196                     }
197                 }
198             }
199         )
200     }
201 
202     fun setStackScroller(stackScrollerController: NotificationStackScrollLayoutController) {
203         mStackScrollerController = stackScrollerController
204         pulseExpanding = stackScrollerController.isPulseExpanding
205         stackScrollerController.setOnPulseHeightChangedListener {
206             val nowExpanding = isPulseExpanding()
207             val changed = nowExpanding != pulseExpanding
208             pulseExpanding = nowExpanding
209             for (listener in wakeUpListeners) {
210                 listener.onPulseExpansionChanged(changed)
211             }
212         }
213     }
214 
215     fun isPulseExpanding(): Boolean = mStackScrollerController.isPulseExpanding
216 
217     /**
218      * @param visible should notifications be visible
219      * @param animate should this change be animated
220      * @param increaseSpeed should the speed be increased of the animation
221      */
222     fun setNotificationsVisibleForExpansion(
223         visible: Boolean,
224         animate: Boolean,
225         increaseSpeed: Boolean
226     ) {
227         mNotificationsVisibleForExpansion = visible
228         updateNotificationVisibility(animate, increaseSpeed)
229         if (!visible && mNotificationsVisible) {
230             // If we stopped expanding and we're still visible because we had a pulse that hasn't
231             // times out, let's release them all to make sure were not stuck in a state where
232             // notifications are visible
233             mHeadsUpManager.releaseAllImmediately()
234         }
235     }
236 
237     fun addListener(listener: WakeUpListener) {
238         wakeUpListeners.add(listener)
239     }
240 
241     fun removeListener(listener: WakeUpListener) {
242         wakeUpListeners.remove(listener)
243     }
244 
245     private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) {
246         // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore
247         var visible = mNotificationsVisibleForExpansion || mHeadsUpManager.hasNotifications()
248         visible = visible && canShowPulsingHuns
249 
250         if (
251             !visible &&
252                 mNotificationsVisible &&
253                 (wakingUp || willWakeUp) &&
254                 outputLinearDozeAmount != 0.0f
255         ) {
256             // let's not make notifications invisible while waking up, otherwise the animation
257             // is strange
258             return
259         }
260         setNotificationsVisible(visible, animate, increaseSpeed)
261     }
262 
263     private fun setNotificationsVisible(
264         visible: Boolean,
265         animate: Boolean,
266         increaseSpeed: Boolean
267     ) {
268         if (mNotificationsVisible == visible) {
269             return
270         }
271         mNotificationsVisible = visible
272         mVisibilityAnimator?.cancel()
273         if (animate) {
274             notifyAnimationStart(visible)
275             startVisibilityAnimation(increaseSpeed)
276         } else {
277             setVisibilityAmount(if (visible) 1.0f else 0.0f)
278         }
279     }
280 
281     override fun onDozeAmountChanged(linear: Float, eased: Float) {
282         logger.logOnDozeAmountChanged(linear = linear, eased = eased)
283         inputLinearDozeAmount = linear
284         inputEasedDozeAmount = eased
285         if (overrideDozeAmountIfAnimatingScreenOff()) {
286             return
287         }
288 
289         if (overrideDozeAmountIfBypass()) {
290             return
291         }
292 
293         if (clearHardDozeAmountOverride()) {
294             return
295         }
296 
297         updateDozeAmount()
298     }
299 
300     private fun setHardDozeAmountOverride(dozing: Boolean, source: String) {
301         logger.logSetDozeAmountOverride(dozing = dozing, source = source)
302         hardDozeAmountOverride = if (dozing) 1f else 0f
303         hardDozeAmountOverrideSource = source
304         updateDozeAmount()
305     }
306 
307     private fun clearHardDozeAmountOverride(): Boolean {
308         if (hardDozeAmountOverride == null) return false
309         hardDozeAmountOverride = null
310         hardDozeAmountOverrideSource = "Cleared: $hardDozeAmountOverrideSource"
311         updateDozeAmount()
312         return true
313     }
314 
315     private fun updateDozeAmount() {
316         // Calculate new doze amount (linear)
317         val newOutputLinearDozeAmount =
318             hardDozeAmountOverride ?: max(inputLinearDozeAmount, delayedDozeAmountOverride)
319         val changed = outputLinearDozeAmount != newOutputLinearDozeAmount
320 
321         // notify when the animation is starting
322         if (
323             newOutputLinearDozeAmount != 1.0f &&
324                 newOutputLinearDozeAmount != 0.0f &&
325                 (outputLinearDozeAmount == 0.0f || outputLinearDozeAmount == 1.0f)
326         ) {
327             // Let's notify the scroller that an animation started
328             notifyAnimationStart(outputLinearDozeAmount == 1.0f)
329         }
330 
331         // Update output doze amount
332         outputLinearDozeAmount = newOutputLinearDozeAmount
333         outputEasedDozeAmount = dozeAmountInterpolator.getInterpolation(outputLinearDozeAmount)
334         logger.logUpdateDozeAmount(
335             inputLinear = inputLinearDozeAmount,
336             delayLinear = delayedDozeAmountOverride,
337             hardOverride = hardDozeAmountOverride,
338             outputLinear = outputLinearDozeAmount,
339             state = statusBarStateController.state,
340             changed = changed
341         )
342         mStackScrollerController.setDozeAmount(outputEasedDozeAmount)
343         updateHideAmount()
344         if (changed && outputLinearDozeAmount == 0.0f) {
345             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
346             setNotificationsVisibleForExpansion(
347                 visible = false,
348                 animate = false,
349                 increaseSpeed = false
350             )
351         }
352     }
353 
354     /**
355      * Notifies the wakeup coordinator that we're waking up.
356      *
357      * [requestDelayedAnimation] is used to request that we delay the start of the wakeup animation
358      * in order to wait for a potential fingerprint authentication to arrive, since unlocking during
359      * the wakeup animation looks chaotic.
360      *
361      * If called with [wakingUp] and [requestDelayedAnimation] both `true`, the [WakeUpListener]s
362      * are guaranteed to receive at least one [WakeUpListener.onDelayedDozeAmountAnimationRunning]
363      * call with `false` at some point in the near future. A call with `true` before that will
364      * happen if the animation is not already running.
365      */
366     fun setWakingUp(
367         wakingUp: Boolean,
368         requestDelayedAnimation: Boolean,
369     ) {
370         logger.logSetWakingUp(wakingUp, requestDelayedAnimation)
371         this.wakingUp = wakingUp
372         if (wakingUp && requestDelayedAnimation) {
373             scheduleDelayedDozeAmountAnimation()
374         }
375     }
376 
377     private fun scheduleDelayedDozeAmountAnimation() {
378         val alreadyRunning = delayedDozeAmountAnimator != null
379         logger.logStartDelayedDozeAmountAnimation(alreadyRunning)
380         if (alreadyRunning) return
381         delayedDozeAmount.setValue(this, 1.0f)
382         delayedDozeAmountAnimator =
383             ObjectAnimator.ofFloat(this, delayedDozeAmount, 0.0f).apply {
384                 interpolator = InterpolatorsAndroidX.LINEAR
385                 duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
386                 startDelay = ShadeViewController.WAKEUP_ANIMATION_DELAY_MS.toLong()
387                 doOnStart {
388                     wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(true) }
389                 }
390                 doOnEnd {
391                     delayedDozeAmountAnimator = null
392                     wakeUpListeners.forEach { it.onDelayedDozeAmountAnimationRunning(false) }
393                 }
394                 start()
395             }
396     }
397 
398     override fun onStateChanged(newState: Int) {
399         logger.logOnStateChanged(newState = newState, storedState = state)
400         if (state == StatusBarState.SHADE && newState == StatusBarState.SHADE) {
401             // The SHADE -> SHADE transition is only possible as part of cancelling the screen-off
402             // animation (e.g. by fingerprint unlock).  This is done because the system is in an
403             // undefined state, so it's an indication that we should do state cleanup. We override
404             // the doze amount to 0f (not dozing) so that the notifications are no longer hidden.
405             // See: UnlockedScreenOffAnimationController.onFinishedWakingUp()
406             setHardDozeAmountOverride(
407                 dozing = false,
408                 source = "Override: Shade->Shade (lock cancelled by unlock)"
409             )
410             this.state = newState
411             return
412         }
413 
414         if (overrideDozeAmountIfAnimatingScreenOff()) {
415             this.state = newState
416             return
417         }
418 
419         if (overrideDozeAmountIfBypass()) {
420             this.state = newState
421             return
422         }
423 
424         maybeClearHardDozeAmountOverrideHidingNotifs()
425 
426         this.state = newState
427     }
428 
429     @VisibleForTesting
430     val statusBarState: Int
431         get() = state
432 
433     override fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) {
434         val collapsedEnough = event.fraction <= 0.9f
435         if (collapsedEnough != this.collapsedEnoughToHide) {
436             val couldShowPulsingHuns = canShowPulsingHuns
437             this.collapsedEnoughToHide = collapsedEnough
438             if (couldShowPulsingHuns && !canShowPulsingHuns) {
439                 updateNotificationVisibility(animate = true, increaseSpeed = true)
440                 mHeadsUpManager.releaseAllImmediately()
441             }
442         }
443     }
444 
445     /**
446      * @return Whether the doze amount was overridden because bypass is enabled. If true, the
447      *   original doze amount should be ignored.
448      */
449     private fun overrideDozeAmountIfBypass(): Boolean {
450         if (bypassController.bypassEnabled) {
451             if (statusBarStateController.state == StatusBarState.KEYGUARD) {
452                 setHardDozeAmountOverride(dozing = true, source = "Override: bypass (keyguard)")
453             } else {
454                 setHardDozeAmountOverride(dozing = false, source = "Override: bypass (shade)")
455             }
456             return true
457         }
458         return false
459     }
460 
461     /**
462      * If the last [setDozeAmount] call was an override to hide notifications, then this call will
463      * check for the set of states that may have caused that override, and if none of them still
464      * apply, and the device is awake or not on the keyguard, then dozeAmount will be reset to 0.
465      * This fixes bugs where the bypass state changing could result in stale overrides, hiding
466      * notifications either on the inside screen or even after unlock.
467      */
468     private fun maybeClearHardDozeAmountOverrideHidingNotifs() {
469         if (hardDozeAmountOverride == 1f) {
470             val onKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
471             val dozing = statusBarStateController.isDozing
472             val bypass = bypassController.bypassEnabled
473             val animating =
474                 screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()
475             // Overrides are set by [overrideDozeAmountIfAnimatingScreenOff] and
476             // [overrideDozeAmountIfBypass] based on 'animating' and 'bypass' respectively, so only
477             // clear the override if both those conditions are cleared.  But also require either
478             // !dozing or !onKeyguard because those conditions should indicate that we intend
479             // notifications to be visible, and thus it is safe to unhide them.
480             val willRemove = (!onKeyguard || !dozing) && !bypass && !animating
481             logger.logMaybeClearHardDozeAmountOverrideHidingNotifs(
482                 willRemove = willRemove,
483                 onKeyguard = onKeyguard,
484                 dozing = dozing,
485                 bypass = bypass,
486                 animating = animating,
487             )
488             if (willRemove) {
489                 clearHardDozeAmountOverride()
490             }
491         }
492     }
493 
494     /**
495      * If we're playing the screen off animation, force the notification doze amount to be 1f (fully
496      * dozing). This is needed so that the notifications aren't briefly visible as the screen turns
497      * off and dozeAmount goes from 1f to 0f.
498      *
499      * @return Whether the doze amount was overridden because we are playing the screen off
500      *   animation. If true, the original doze amount should be ignored.
501      */
502     private fun overrideDozeAmountIfAnimatingScreenOff(): Boolean {
503         if (screenOffAnimationController.overrideNotificationsFullyDozingOnKeyguard()) {
504             setHardDozeAmountOverride(dozing = true, source = "Override: animating screen off")
505             return true
506         }
507 
508         return false
509     }
510 
511     private fun startVisibilityAnimation(increaseSpeed: Boolean) {
512         if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
513             mVisibilityInterpolator =
514                 if (mNotificationsVisible) Interpolators.TOUCH_RESPONSE
515                 else Interpolators.FAST_OUT_SLOW_IN_REVERSE
516         }
517         val target = if (mNotificationsVisible) 1.0f else 0.0f
518         val visibilityAnimator = ObjectAnimator.ofFloat(this, notificationVisibility, target)
519         visibilityAnimator.interpolator = InterpolatorsAndroidX.LINEAR
520         var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
521         if (increaseSpeed) {
522             duration = (duration.toFloat() / 1.5F).toLong()
523         }
524         visibilityAnimator.duration = duration
525         visibilityAnimator.start()
526         mVisibilityAnimator = visibilityAnimator
527     }
528 
529     private fun setVisibilityAmount(visibilityAmount: Float) {
530         logger.logSetVisibilityAmount(visibilityAmount)
531         mLinearVisibilityAmount = visibilityAmount
532         mVisibilityAmount = mVisibilityInterpolator.getInterpolation(visibilityAmount)
533         handleAnimationFinished()
534         updateHideAmount()
535     }
536 
537     private fun handleAnimationFinished() {
538         if (outputLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
539             mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
540             mEntrySetToClearWhenFinished.clear()
541         }
542     }
543 
544     private fun updateHideAmount() {
545         val linearAmount = min(1.0f - mLinearVisibilityAmount, outputLinearDozeAmount)
546         val amount = min(1.0f - mVisibilityAmount, outputEasedDozeAmount)
547         logger.logSetHideAmount(linearAmount)
548         mStackScrollerController.setHideAmount(linearAmount, amount)
549         notificationsFullyHidden = linearAmount == 1.0f
550     }
551 
552     private fun notifyAnimationStart(awake: Boolean) {
553         mStackScrollerController.notifyHideAnimationStart(!awake)
554     }
555 
556     override fun onDozingChanged(isDozing: Boolean) {
557         if (isDozing) {
558             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
559         }
560     }
561 
562     override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
563         var animate = shouldAnimateVisibility()
564         if (!isHeadsUp) {
565             if (outputLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
566                 if (entry.isRowDismissed) {
567                     // if we animate, we see the shelf briefly visible. Instead we fully animate
568                     // the notification and its background out
569                     animate = false
570                 } else if (!wakingUp && !willWakeUp) {
571                     // TODO: look that this is done properly and not by anyone else
572                     entry.setHeadsUpAnimatingAway(true)
573                     mEntrySetToClearWhenFinished.add(entry)
574                 }
575             }
576         } else if (mEntrySetToClearWhenFinished.contains(entry)) {
577             mEntrySetToClearWhenFinished.remove(entry)
578             entry.setHeadsUpAnimatingAway(false)
579         }
580         updateNotificationVisibility(animate, increaseSpeed = false)
581     }
582 
583     private fun shouldAnimateVisibility() =
584         dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
585 
586     override fun dump(pw: PrintWriter, args: Array<out String>) {
587         pw.println("inputLinearDozeAmount: $inputLinearDozeAmount")
588         pw.println("inputEasedDozeAmount: $inputEasedDozeAmount")
589         pw.println("delayedDozeAmountOverride: $delayedDozeAmountOverride")
590         pw.println("hardDozeAmountOverride: $hardDozeAmountOverride")
591         pw.println("hardDozeAmountOverrideSource: $hardDozeAmountOverrideSource")
592         pw.println("outputLinearDozeAmount: $outputLinearDozeAmount")
593         pw.println("outputEasedDozeAmount: $outputEasedDozeAmount")
594         pw.println("mNotificationVisibleAmount: $mNotificationVisibleAmount")
595         pw.println("mNotificationsVisible: $mNotificationsVisible")
596         pw.println("mNotificationsVisibleForExpansion: $mNotificationsVisibleForExpansion")
597         pw.println("mVisibilityAmount: $mVisibilityAmount")
598         pw.println("mLinearVisibilityAmount: $mLinearVisibilityAmount")
599         pw.println("pulseExpanding: $pulseExpanding")
600         pw.println("state: ${StatusBarState.toString(state)}")
601         pw.println("fullyAwake: $fullyAwake")
602         pw.println("wakingUp: $wakingUp")
603         pw.println("willWakeUp: $willWakeUp")
604         pw.println("collapsedEnoughToHide: $collapsedEnoughToHide")
605         pw.println("pulsing: $pulsing")
606         pw.println("notificationsFullyHidden: $notificationsFullyHidden")
607         pw.println("canShowPulsingHuns: $canShowPulsingHuns")
608     }
609 
610     fun logDelayingClockWakeUpAnimation(delayingAnimation: Boolean) {
611         logger.logDelayingClockWakeUpAnimation(delayingAnimation)
612     }
613 
614     interface WakeUpListener {
615         /** Called whenever the notifications are fully hidden or shown */
616         @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {}
617 
618         /**
619          * Called whenever the pulseExpansion changes
620          *
621          * @param expandingChanged if the user has started or stopped expanding
622          */
623         @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
624 
625         /**
626          * Called when the animator started by [scheduleDelayedDozeAmountAnimation] begins running
627          * after the start delay, or after it ends/is cancelled.
628          */
629         @JvmDefault fun onDelayedDozeAmountAnimationRunning(running: Boolean) {}
630     }
631 
632     companion object {
633         private val notificationVisibility =
634             object : FloatProperty<NotificationWakeUpCoordinator>("notificationVisibility") {
635 
636                 override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) {
637                     coordinator.setVisibilityAmount(value)
638                 }
639 
640                 override fun get(coordinator: NotificationWakeUpCoordinator): Float {
641                     return coordinator.mLinearVisibilityAmount
642                 }
643             }
644 
645         private val delayedDozeAmount =
646             object : FloatProperty<NotificationWakeUpCoordinator>("delayedDozeAmount") {
647 
648                 override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) {
649                     coordinator.delayedDozeAmountOverride = value
650                     coordinator.logger.logSetDelayDozeAmountOverride(value)
651                     coordinator.updateDozeAmount()
652                 }
653 
654                 override fun get(coordinator: NotificationWakeUpCoordinator): Float {
655                     return coordinator.delayedDozeAmountOverride
656                 }
657             }
658     }
659 }
660