• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }