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.content.Context
21 import android.util.FloatProperty
22 import com.android.systemui.Interpolators
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.NotificationStackScrollLayout
27 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
28 import com.android.systemui.statusbar.phone.DozeParameters
29 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
30 import com.android.systemui.statusbar.phone.KeyguardBypassController
31 import com.android.systemui.statusbar.phone.NotificationIconAreaController
32 import com.android.systemui.statusbar.phone.PanelExpansionListener
33 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
34 
35 import javax.inject.Inject
36 import javax.inject.Singleton
37 
38 @Singleton
39 class NotificationWakeUpCoordinator @Inject constructor(
40         private val mContext: Context,
41         private val mHeadsUpManagerPhone: HeadsUpManagerPhone,
42         private val statusBarStateController: StatusBarStateController,
43         private val bypassController: KeyguardBypassController)
44     : OnHeadsUpChangedListener, StatusBarStateController.StateListener,
45         PanelExpansionListener {
46 
47     private val mNotificationVisibility
48             = object : FloatProperty<NotificationWakeUpCoordinator>("notificationVisibility") {
49 
setValuenull50         override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) {
51             coordinator.setVisibilityAmount(value)
52         }
53 
getnull54         override fun get(coordinator: NotificationWakeUpCoordinator): Float? {
55             return coordinator.mLinearVisibilityAmount
56         }
57     }
58     private lateinit var mStackScroller: NotificationStackScrollLayout
59     private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
60 
61     private var mLinearDozeAmount: Float = 0.0f
62     private var mDozeAmount: Float = 0.0f
63     private var mNotificationVisibleAmount = 0.0f
64     private var mNotificationsVisible = false
65     private var mNotificationsVisibleForExpansion = false
66     private var mVisibilityAnimator: ObjectAnimator? = null
67     private var mVisibilityAmount = 0.0f
68     private var mLinearVisibilityAmount = 0.0f
69     private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
70     private val mDozeParameters: DozeParameters
71     private var pulseExpanding: Boolean = false
72     private val wakeUpListeners = arrayListOf<WakeUpListener>()
73     private var state: Int = StatusBarState.KEYGUARD
74 
75     var fullyAwake: Boolean = false
76 
77     var wakingUp = false
78         set(value) {
79             field = value
80             willWakeUp = false
81             if (value) {
82                 if (mNotificationsVisible && !mNotificationsVisibleForExpansion
83                         && !bypassController.bypassEnabled) {
84                     // We're waking up while pulsing, let's make sure the animation looks nice
85                     mStackScroller.wakeUpFromPulse();
86                 }
87                 if (bypassController.bypassEnabled && !mNotificationsVisible) {
88                     // Let's make sure our huns become visible once we are waking up in case
89                     // they were blocked by the proximity sensor
90                     updateNotificationVisibility(animate = shouldAnimateVisibility(),
91                             increaseSpeed = false)
92                 }
93             }
94         }
95 
96     var willWakeUp = false
97         set(value) {
98             if (!value || mDozeAmount != 0.0f) {
99                 field = value
100             }
101         }
102 
103     private var collapsedEnoughToHide: Boolean = false
104     lateinit var iconAreaController : NotificationIconAreaController
105 
106     var pulsing: Boolean = false
107         set(value) {
108             field = value
109             if (value) {
110                 // Only when setting pulsing to true we want an immediate update, since we get
111                 // this already when the doze service finishes which is usually before we get
112                 // the waking up callback
113                 updateNotificationVisibility(animate = shouldAnimateVisibility(),
114                         increaseSpeed = false)
115             }
116         }
117 
118     var notificationsFullyHidden: Boolean = false
119         private set(value) {
120             if (field != value) {
121                 field = value
122                 for (listener in wakeUpListeners) {
123                     listener.onFullyHiddenChanged(value)
124                 }
125             }
126         }
127     /**
128      * True if we can show pulsing heads up notifications
129      */
130     var canShowPulsingHuns: Boolean = false
131         private set
132         get() {
133             var canShow = pulsing
134             if (bypassController.bypassEnabled) {
135                 // We also allow pulsing on the lock screen!
136                 canShow = canShow || (wakingUp || willWakeUp || fullyAwake)
137                         && statusBarStateController.state == StatusBarState.KEYGUARD
138                 // We want to hide the notifications when collapsed too much
139                 if (collapsedEnoughToHide) {
140                     canShow = false
141                 }
142             }
143             return canShow
144         }
145 
146     init {
147         mHeadsUpManagerPhone.addListener(this)
148         statusBarStateController.addCallback(this)
149         mDozeParameters = DozeParameters.getInstance(mContext)
150         addListener(object : WakeUpListener {
onFullyHiddenChangednull151             override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
152                 if (isFullyHidden && mNotificationsVisibleForExpansion) {
153                     // When the notification becomes fully invisible, let's make sure our expansion
154                     // flag also changes. This can happen if the bouncer shows when dragging down
155                     // and then the screen turning off, where we don't reset this state.
156                     setNotificationsVisibleForExpansion(visible = false, animate = false,
157                             increaseSpeed = false)
158                 }
159             }
160         });
161     }
162 
setStackScrollernull163     fun setStackScroller(stackScroller: NotificationStackScrollLayout) {
164         mStackScroller = stackScroller
165         pulseExpanding = stackScroller.isPulseExpanding
166         stackScroller.setOnPulseHeightChangedListener {
167             val nowExpanding = isPulseExpanding()
168             val changed = nowExpanding != pulseExpanding
169             pulseExpanding = nowExpanding
170             for (listener in wakeUpListeners) {
171                 listener.onPulseExpansionChanged(changed)
172             }
173         }
174     }
175 
isPulseExpandingnull176     fun isPulseExpanding(): Boolean = mStackScroller.isPulseExpanding
177 
178     /**
179      * @param visible should notifications be visible
180      * @param animate should this change be animated
181      * @param increaseSpeed should the speed be increased of the animation
182      */
183     fun setNotificationsVisibleForExpansion(visible: Boolean, animate: Boolean,
184                                                     increaseSpeed: Boolean) {
185         mNotificationsVisibleForExpansion = visible
186         updateNotificationVisibility(animate, increaseSpeed)
187         if (!visible && mNotificationsVisible) {
188             // If we stopped expanding and we're still visible because we had a pulse that hasn't
189             // times out, let's release them all to make sure were not stuck in a state where
190             // notifications are visible
191             mHeadsUpManagerPhone.releaseAllImmediately()
192         }
193     }
194 
addListenernull195     fun addListener(listener: WakeUpListener) {
196         wakeUpListeners.add(listener);
197     }
198 
removeListenernull199     fun removeListener(listener: WakeUpListener) {
200         wakeUpListeners.remove(listener);
201     }
202 
updateNotificationVisibilitynull203     private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) {
204         // TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore
205         var visible = mNotificationsVisibleForExpansion || mHeadsUpManagerPhone.hasNotifications()
206         visible = visible && canShowPulsingHuns
207 
208         if (!visible && mNotificationsVisible && (wakingUp || willWakeUp) && mDozeAmount != 0.0f) {
209             // let's not make notifications invisible while waking up, otherwise the animation
210             // is strange
211             return;
212         }
213         setNotificationsVisible(visible, animate, increaseSpeed)
214     }
215 
setNotificationsVisiblenull216     private fun setNotificationsVisible(visible: Boolean, animate: Boolean,
217                                         increaseSpeed: Boolean) {
218         if (mNotificationsVisible == visible) {
219             return
220         }
221         mNotificationsVisible = visible
222         mVisibilityAnimator?.cancel();
223         if (animate) {
224             notifyAnimationStart(visible)
225             startVisibilityAnimation(increaseSpeed)
226         } else {
227             setVisibilityAmount(if (visible) 1.0f else 0.0f)
228         }
229     }
230 
onDozeAmountChangednull231     override fun onDozeAmountChanged(linear: Float, eased: Float) {
232         if (updateDozeAmountIfBypass()) {
233             return
234         }
235         if (linear != 1.0f && linear != 0.0f
236                 && (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) {
237             // Let's notify the scroller that an animation started
238             notifyAnimationStart(mLinearDozeAmount == 1.0f)
239         }
240         setDozeAmount(linear, eased)
241     }
242 
setDozeAmountnull243     fun setDozeAmount(linear: Float, eased: Float) {
244         val changed = linear != mLinearDozeAmount
245         mLinearDozeAmount = linear
246         mDozeAmount = eased
247         mStackScroller.setDozeAmount(mDozeAmount)
248         updateHideAmount()
249         if (changed && linear == 0.0f) {
250             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false);
251             setNotificationsVisibleForExpansion(visible = false, animate = false,
252                     increaseSpeed = false)
253         }
254     }
255 
onStateChangednull256     override fun onStateChanged(newState: Int) {
257         updateDozeAmountIfBypass();
258         if (bypassController.bypassEnabled &&
259                 newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED
260                 && (!statusBarStateController.isDozing || shouldAnimateVisibility())) {
261             // We're leaving shade locked. Let's animate the notifications away
262             setNotificationsVisible(visible = true, increaseSpeed = false, animate = false)
263             setNotificationsVisible(visible = false, increaseSpeed = false, animate = true)
264         }
265         this.state = newState
266     }
267 
onPanelExpansionChangednull268     override fun onPanelExpansionChanged(expansion: Float, tracking: Boolean) {
269         val collapsedEnough = expansion <= 0.9f
270         if (collapsedEnough != this.collapsedEnoughToHide) {
271             val couldShowPulsingHuns = canShowPulsingHuns;
272             this.collapsedEnoughToHide = collapsedEnough
273             if (couldShowPulsingHuns && !canShowPulsingHuns) {
274                 updateNotificationVisibility(animate = true, increaseSpeed = true)
275                 mHeadsUpManagerPhone.releaseAllImmediately()
276             }
277         }
278     }
279 
updateDozeAmountIfBypassnull280     private fun updateDozeAmountIfBypass(): Boolean {
281         if (bypassController.bypassEnabled) {
282             var amount = 1.0f;
283             if (statusBarStateController.state == StatusBarState.SHADE
284                     || statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
285                 amount = 0.0f;
286             }
287             setDozeAmount(amount,  amount)
288             return true
289         }
290         return false
291     }
292 
startVisibilityAnimationnull293     private fun startVisibilityAnimation(increaseSpeed: Boolean) {
294         if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
295             mVisibilityInterpolator = if (mNotificationsVisible)
296                 Interpolators.TOUCH_RESPONSE
297             else
298                 Interpolators.FAST_OUT_SLOW_IN_REVERSE
299         }
300         val target = if (mNotificationsVisible) 1.0f else 0.0f
301         val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target)
302         visibilityAnimator.setInterpolator(Interpolators.LINEAR)
303         var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
304         if (increaseSpeed) {
305             duration = (duration.toFloat() / 1.5F).toLong();
306         }
307         visibilityAnimator.setDuration(duration)
308         visibilityAnimator.start()
309         mVisibilityAnimator = visibilityAnimator
310     }
311 
setVisibilityAmountnull312     private fun setVisibilityAmount(visibilityAmount: Float) {
313         mLinearVisibilityAmount = visibilityAmount
314         mVisibilityAmount = mVisibilityInterpolator.getInterpolation(
315                 visibilityAmount)
316         handleAnimationFinished();
317         updateHideAmount()
318     }
319 
handleAnimationFinishednull320     private fun handleAnimationFinished() {
321         if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
322             mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
323             mEntrySetToClearWhenFinished.clear()
324         }
325     }
326 
getWakeUpHeightnull327     fun getWakeUpHeight() : Float {
328         return mStackScroller.wakeUpHeight
329     }
330 
updateHideAmountnull331     private fun updateHideAmount() {
332         val linearAmount = Math.min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount)
333         val amount = Math.min(1.0f - mVisibilityAmount, mDozeAmount)
334         mStackScroller.setHideAmount(linearAmount, amount)
335         notificationsFullyHidden = linearAmount == 1.0f;
336     }
337 
notifyAnimationStartnull338     private fun notifyAnimationStart(awake: Boolean) {
339         mStackScroller.notifyHideAnimationStart(!awake)
340     }
341 
onDozingChangednull342     override fun onDozingChanged(isDozing: Boolean) {
343         if (isDozing) {
344             setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
345         }
346     }
347 
348     /**
349      * Set the height how tall notifications are pulsing. This is only set whenever we are expanding
350      * from a pulse and determines how much the notifications are expanded.
351      */
setPulseHeightnull352     fun setPulseHeight(height: Float): Float {
353         val overflow = mStackScroller.setPulseHeight(height)
354         //  no overflow for the bypass experience
355         return if (bypassController.bypassEnabled) 0.0f else overflow
356     }
357 
onHeadsUpStateChangednull358     override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
359         var animate = shouldAnimateVisibility()
360         if (!isHeadsUp) {
361             if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
362                 if (entry.isRowDismissed) {
363                     // if we animate, we see the shelf briefly visible. Instead we fully animate
364                     // the notification and its background out
365                     animate = false
366                 } else if (!wakingUp && !willWakeUp){
367                     // TODO: look that this is done properly and not by anyone else
368                     entry.setHeadsUpAnimatingAway(true)
369                     mEntrySetToClearWhenFinished.add(entry)
370                 }
371             }
372         } else if (mEntrySetToClearWhenFinished.contains(entry)) {
373             mEntrySetToClearWhenFinished.remove(entry)
374             entry.setHeadsUpAnimatingAway(false)
375         }
376         updateNotificationVisibility(animate, increaseSpeed = false)
377     }
378 
shouldAnimateVisibilitynull379     private fun shouldAnimateVisibility() =
380             mDozeParameters.getAlwaysOn() && !mDozeParameters.getDisplayNeedsBlanking()
381 
382     interface WakeUpListener {
383         /**
384          * Called whenever the notifications are fully hidden or shown
385          */
386         @JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {}
387 
388         /**
389          * Called whenever the pulseExpansion changes
390          * @param expandingChanged if the user has started or stopped expanding
391          */
392         @JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
393     }
394 }