• 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.Interpolators
22 import com.android.systemui.plugins.statusbar.StatusBarStateController
23 import com.android.systemui.statusbar.StatusBarState
24 import com.android.systemui.statusbar.notification.collection.NotificationEntry
25 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
26 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
27 import com.android.systemui.statusbar.phone.DozeParameters
28 import com.android.systemui.statusbar.phone.KeyguardBypassController
29 import com.android.systemui.statusbar.phone.NotificationIconAreaController
30 import com.android.systemui.statusbar.phone.PanelExpansionListener
31 import com.android.systemui.statusbar.policy.HeadsUpManager
32 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
33 
34 import javax.inject.Inject
35 import javax.inject.Singleton
36 
37 @Singleton
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 ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, PanelExpansionListener {
44 
45     private val mNotificationVisibility = object : FloatProperty<NotificationWakeUpCoordinator>(
46         "notificationVisibility") {
47 
setValuenull48         override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) {
49             coordinator.setVisibilityAmount(value)
50         }
51 
getnull52         override fun get(coordinator: NotificationWakeUpCoordinator): Float? {
53             return coordinator.mLinearVisibilityAmount
54         }
55     }
56     private lateinit var mStackScroller: NotificationStackScrollLayout
57     private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
58 
59     private var mLinearDozeAmount: Float = 0.0f
60     private var mDozeAmount: Float = 0.0f
61     private var mNotificationVisibleAmount = 0.0f
62     private var mNotificationsVisible = false
63     private var mNotificationsVisibleForExpansion = false
64     private var mVisibilityAnimator: ObjectAnimator? = null
65     private var mVisibilityAmount = 0.0f
66     private var mLinearVisibilityAmount = 0.0f
67     private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
68     private var pulseExpanding: Boolean = false
69     private val wakeUpListeners = arrayListOf<WakeUpListener>()
70     private var state: Int = StatusBarState.KEYGUARD
71 
72     var fullyAwake: Boolean = false
73 
74     var wakingUp = false
75         set(value) {
76             field = value
77             willWakeUp = false
78             if (value) {
79                 if (mNotificationsVisible && !mNotificationsVisibleForExpansion &&
80                     !bypassController.bypassEnabled) {
81                     // We're waking up while pulsing, let's make sure the animation looks nice
82                     mStackScroller.wakeUpFromPulse()
83                 }
84                 if (bypassController.bypassEnabled && !mNotificationsVisible) {
85                     // Let's make sure our huns become visible once we are waking up in case
86                     // they were blocked by the proximity sensor
87                     updateNotificationVisibility(animate = shouldAnimateVisibility(),
88                             increaseSpeed = false)
89                 }
90             }
91         }
92 
93     var willWakeUp = false
94         set(value) {
95             if (!value || mDozeAmount != 0.0f) {
96                 field = value
97             }
98         }
99 
100     private var collapsedEnoughToHide: Boolean = false
101     lateinit var iconAreaController: NotificationIconAreaController
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(stackScroller: NotificationStackScrollLayout) {
160         mStackScroller = stackScroller
161         pulseExpanding = stackScroller.isPulseExpanding
162         stackScroller.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 = mStackScroller.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 (updateDozeAmountIfBypass()) {
238             return
239         }
240         if (linear != 1.0f && linear != 0.0f &&
241             (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) {
242             // Let's notify the scroller that an animation started
243             notifyAnimationStart(mLinearDozeAmount == 1.0f)
244         }
245         setDozeAmount(linear, eased)
246     }
247 
setDozeAmountnull248     fun setDozeAmount(linear: Float, eased: Float) {
249         val changed = linear != mLinearDozeAmount
250         mLinearDozeAmount = linear
251         mDozeAmount = eased
252         mStackScroller.setDozeAmount(mDozeAmount)
253         updateHideAmount()
254         if (changed && linear == 0.0f) {
255             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
256             setNotificationsVisibleForExpansion(visible = false, animate = false,
257                     increaseSpeed = false)
258         }
259     }
260 
onStateChangednull261     override fun onStateChanged(newState: Int) {
262         updateDozeAmountIfBypass()
263         if (bypassController.bypassEnabled &&
264                 newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED &&
265             (!statusBarStateController.isDozing || shouldAnimateVisibility())) {
266             // We're leaving shade locked. Let's animate the notifications away
267             setNotificationsVisible(visible = true, increaseSpeed = false, animate = false)
268             setNotificationsVisible(visible = false, increaseSpeed = false, animate = true)
269         }
270         this.state = newState
271     }
272 
onPanelExpansionChangednull273     override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) {
274         val collapsedEnough = expansion <= 0.9f
275         if (collapsedEnough != this.collapsedEnoughToHide) {
276             val couldShowPulsingHuns = canShowPulsingHuns
277             this.collapsedEnoughToHide = collapsedEnough
278             if (couldShowPulsingHuns && !canShowPulsingHuns) {
279                 updateNotificationVisibility(animate = true, increaseSpeed = true)
280                 mHeadsUpManager.releaseAllImmediately()
281             }
282         }
283     }
284 
updateDozeAmountIfBypassnull285     private fun updateDozeAmountIfBypass(): Boolean {
286         if (bypassController.bypassEnabled) {
287             var amount = 1.0f
288             if (statusBarStateController.state == StatusBarState.SHADE ||
289                 statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
290                 amount = 0.0f
291             }
292             setDozeAmount(amount, amount)
293             return true
294         }
295         return false
296     }
297 
startVisibilityAnimationnull298     private fun startVisibilityAnimation(increaseSpeed: Boolean) {
299         if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
300             mVisibilityInterpolator = if (mNotificationsVisible)
301                 Interpolators.TOUCH_RESPONSE
302             else
303                 Interpolators.FAST_OUT_SLOW_IN_REVERSE
304         }
305         val target = if (mNotificationsVisible) 1.0f else 0.0f
306         val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target)
307         visibilityAnimator.setInterpolator(Interpolators.LINEAR)
308         var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
309         if (increaseSpeed) {
310             duration = (duration.toFloat() / 1.5F).toLong()
311         }
312         visibilityAnimator.setDuration(duration)
313         visibilityAnimator.start()
314         mVisibilityAnimator = visibilityAnimator
315     }
316 
setVisibilityAmountnull317     private fun setVisibilityAmount(visibilityAmount: Float) {
318         mLinearVisibilityAmount = visibilityAmount
319         mVisibilityAmount = mVisibilityInterpolator.getInterpolation(
320                 visibilityAmount)
321         handleAnimationFinished()
322         updateHideAmount()
323     }
324 
handleAnimationFinishednull325     private fun handleAnimationFinished() {
326         if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
327             mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
328             mEntrySetToClearWhenFinished.clear()
329         }
330     }
331 
getWakeUpHeightnull332     fun getWakeUpHeight(): Float {
333         return mStackScroller.wakeUpHeight
334     }
335 
updateHideAmountnull336     private fun updateHideAmount() {
337         val linearAmount = Math.min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount)
338         val amount = Math.min(1.0f - mVisibilityAmount, mDozeAmount)
339         mStackScroller.setHideAmount(linearAmount, amount)
340         notificationsFullyHidden = linearAmount == 1.0f
341     }
342 
notifyAnimationStartnull343     private fun notifyAnimationStart(awake: Boolean) {
344         mStackScroller.notifyHideAnimationStart(!awake)
345     }
346 
onDozingChangednull347     override fun onDozingChanged(isDozing: Boolean) {
348         if (isDozing) {
349             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
350         }
351     }
352 
353     /**
354      * Set the height how tall notifications are pulsing. This is only set whenever we are expanding
355      * from a pulse and determines how much the notifications are expanded.
356      */
setPulseHeightnull357     fun setPulseHeight(height: Float): Float {
358         val overflow = mStackScroller.setPulseHeight(height)
359         //  no overflow for the bypass experience
360         return if (bypassController.bypassEnabled) 0.0f else overflow
361     }
362 
onHeadsUpStateChangednull363     override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
364         var animate = shouldAnimateVisibility()
365         if (!isHeadsUp) {
366             if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
367                 if (entry.isRowDismissed) {
368                     // if we animate, we see the shelf briefly visible. Instead we fully animate
369                     // the notification and its background out
370                     animate = false
371                 } else if (!wakingUp && !willWakeUp) {
372                     // TODO: look that this is done properly and not by anyone else
373                     entry.setHeadsUpAnimatingAway(true)
374                     mEntrySetToClearWhenFinished.add(entry)
375                 }
376             }
377         } else if (mEntrySetToClearWhenFinished.contains(entry)) {
378             mEntrySetToClearWhenFinished.remove(entry)
379             entry.setHeadsUpAnimatingAway(false)
380         }
381         updateNotificationVisibility(animate, increaseSpeed = false)
382     }
383 
shouldAnimateVisibilitynull384     private fun shouldAnimateVisibility() =
385             dozeParameters.getAlwaysOn() && !dozeParameters.getDisplayNeedsBlanking()
386 
387     interface WakeUpListener {
388         /**
389          * Called whenever the notifications are fully hidden or shown
390          */
391         @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {}
392 
393         /**
394          * Called whenever the pulseExpansion changes
395          * @param expandingChanged if the user has started or stopped expanding
396          */
397         @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
398     }
399 }