blob: f7e5bcf4769c2a6af074d2a517edd01fd8aedc6e [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.systemui.statusbar.notification
import android.animation.ObjectAnimator
import android.content.Context
import android.util.FloatProperty
import com.android.systemui.Interpolators
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.StackStateAnimator
import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NotificationWakeUpCoordinator @Inject constructor(
private val mContext: Context,
private val mHeadsUpManagerPhone: HeadsUpManagerPhone,
private val statusBarStateController: StatusBarStateController,
private val bypassController: KeyguardBypassController)
: OnHeadsUpChangedListener, StatusBarStateController.StateListener {
private val mNotificationVisibility
= object : FloatProperty<NotificationWakeUpCoordinator>("notificationVisibility") {
override fun setValue(coordinator: NotificationWakeUpCoordinator, value: Float) {
coordinator.setVisibilityAmount(value)
}
override fun get(coordinator: NotificationWakeUpCoordinator): Float? {
return coordinator.mLinearVisibilityAmount
}
}
private lateinit var mStackScroller: NotificationStackScrollLayout
private var mVisibilityInterpolator = Interpolators.FAST_OUT_SLOW_IN_REVERSE
private var mLinearDozeAmount: Float = 0.0f
private var mDozeAmount: Float = 0.0f
private var mNotificationVisibleAmount = 0.0f
private var mNotificationsVisible = false
private var mNotificationsVisibleForExpansion = false
private var mVisibilityAnimator: ObjectAnimator? = null
private var mVisibilityAmount = 0.0f
private var mLinearVisibilityAmount = 0.0f
private val mEntrySetToClearWhenFinished = mutableSetOf<NotificationEntry>()
private val mDozeParameters: DozeParameters
private var pulseExpanding: Boolean = false
private val wakeUpListeners = arrayListOf<WakeUpListener>()
private var state: Int = StatusBarState.KEYGUARD
var fullyAwake: Boolean = false
var wakingUp = false
set(value) {
field = value
willWakeUp = false
if (value) {
if (mNotificationsVisible && !mNotificationsVisibleForExpansion
&& !bypassController.bypassEnabled) {
// We're waking up while pulsing, let's make sure the animation looks nice
mStackScroller.wakeUpFromPulse();
}
if (bypassController.bypassEnabled && !mNotificationsVisible) {
// Let's make sure our huns become visible once we are waking up in case
// they were blocked by the proximity sensor
updateNotificationVisibility(animate = shouldAnimateVisibility(),
increaseSpeed = false)
}
}
}
var willWakeUp = false
set(value) {
if (!value || mDozeAmount != 0.0f) {
field = value
}
}
lateinit var iconAreaController : NotificationIconAreaController
var pulsing: Boolean = false
set(value) {
field = value
if (value) {
// Only when setting pulsing to true we want an immediate update, since we get
// this already when the doze service finishes which is usually before we get
// the waking up callback
updateNotificationVisibility(animate = shouldAnimateVisibility(),
increaseSpeed = false)
}
}
var notificationsFullyHidden: Boolean = false
private set(value) {
if (field != value) {
field = value
for (listener in wakeUpListeners) {
listener.onFullyHiddenChanged(value)
}
}
}
/**
* True if we can show pulsing heads up notifications
*/
var canShowPulsingHuns: Boolean = false
private set
get() {
var canShow = pulsing
if (bypassController.bypassEnabled) {
// We also allow pulsing on the lock screen!
canShow = canShow || (wakingUp || willWakeUp || fullyAwake)
&& statusBarStateController.state == StatusBarState.KEYGUARD
}
return canShow
}
init {
mHeadsUpManagerPhone.addListener(this)
statusBarStateController.addCallback(this)
mDozeParameters = DozeParameters.getInstance(mContext)
}
fun setStackScroller(stackScroller: NotificationStackScrollLayout) {
mStackScroller = stackScroller
pulseExpanding = stackScroller.isPulseExpanding
stackScroller.setOnPulseHeightChangedListener {
val nowExpanding = isPulseExpanding()
val changed = nowExpanding != pulseExpanding
pulseExpanding = nowExpanding
for (listener in wakeUpListeners) {
listener.onPulseExpansionChanged(changed)
}
}
}
fun isPulseExpanding(): Boolean = mStackScroller.isPulseExpanding
/**
* @param visible should notifications be visible
* @param animate should this change be animated
* @param increaseSpeed should the speed be increased of the animation
*/
fun setNotificationsVisibleForExpansion(visible: Boolean, animate: Boolean,
increaseSpeed: Boolean) {
mNotificationsVisibleForExpansion = visible
updateNotificationVisibility(animate, increaseSpeed)
if (!visible && mNotificationsVisible) {
// If we stopped expanding and we're still visible because we had a pulse that hasn't
// times out, let's release them all to make sure were not stuck in a state where
// notifications are visible
mHeadsUpManagerPhone.releaseAllImmediately()
}
}
fun addListener(listener: WakeUpListener) {
wakeUpListeners.add(listener);
}
fun removeListener(listener: WakeUpListener) {
wakeUpListeners.remove(listener);
}
private fun updateNotificationVisibility(animate: Boolean, increaseSpeed: Boolean) {
// TODO: handle Lockscreen wakeup for bypass when we're not pulsing anymore
var visible = mNotificationsVisibleForExpansion || mHeadsUpManagerPhone.hasNotifications()
visible = visible && canShowPulsingHuns
if (!visible && mNotificationsVisible && (wakingUp || willWakeUp) && mDozeAmount != 0.0f) {
// let's not make notifications invisible while waking up, otherwise the animation
// is strange
return;
}
setNotificationsVisible(visible, animate, increaseSpeed)
}
private fun setNotificationsVisible(visible: Boolean, animate: Boolean,
increaseSpeed: Boolean) {
if (mNotificationsVisible == visible) {
return
}
mNotificationsVisible = visible
mVisibilityAnimator?.cancel();
if (animate) {
notifyAnimationStart(visible)
startVisibilityAnimation(increaseSpeed)
} else {
setVisibilityAmount(if (visible) 1.0f else 0.0f)
}
}
override fun onDozeAmountChanged(linear: Float, eased: Float) {
if (updateDozeAmountIfBypass()) {
return
}
if (linear != 1.0f && linear != 0.0f
&& (mLinearDozeAmount == 0.0f || mLinearDozeAmount == 1.0f)) {
// Let's notify the scroller that an animation started
notifyAnimationStart(mLinearDozeAmount == 1.0f)
}
setDozeAmount(linear, eased)
}
fun setDozeAmount(linear: Float, eased: Float) {
val changed = linear != mLinearDozeAmount
mLinearDozeAmount = linear
mDozeAmount = eased
mStackScroller.setDozeAmount(mDozeAmount)
updateHideAmount()
if (changed && linear == 0.0f) {
setNotificationsVisible(visible = false, animate = false, increaseSpeed = false);
setNotificationsVisibleForExpansion(visible = false, animate = false,
increaseSpeed = false)
}
}
override fun onStateChanged(newState: Int) {
updateDozeAmountIfBypass();
if (bypassController.bypassEnabled &&
newState == StatusBarState.KEYGUARD && state == StatusBarState.SHADE_LOCKED
&& (!statusBarStateController.isDozing || shouldAnimateVisibility())) {
// We're leaving shade locked. Let's animate the notifications away
setNotificationsVisible(visible = true, increaseSpeed = false, animate = false)
setNotificationsVisible(visible = false, increaseSpeed = false, animate = true)
}
this.state = newState
}
private fun updateDozeAmountIfBypass(): Boolean {
if (bypassController.bypassEnabled) {
var amount = 1.0f;
if (statusBarStateController.state == StatusBarState.SHADE
|| statusBarStateController.state == StatusBarState.SHADE_LOCKED) {
amount = 0.0f;
}
setDozeAmount(amount, amount)
return true
}
return false
}
private fun startVisibilityAnimation(increaseSpeed: Boolean) {
if (mNotificationVisibleAmount == 0f || mNotificationVisibleAmount == 1f) {
mVisibilityInterpolator = if (mNotificationsVisible)
Interpolators.TOUCH_RESPONSE
else
Interpolators.FAST_OUT_SLOW_IN_REVERSE
}
val target = if (mNotificationsVisible) 1.0f else 0.0f
val visibilityAnimator = ObjectAnimator.ofFloat(this, mNotificationVisibility, target)
visibilityAnimator.setInterpolator(Interpolators.LINEAR)
var duration = StackStateAnimator.ANIMATION_DURATION_WAKEUP.toLong()
if (increaseSpeed) {
duration = (duration.toFloat() / 1.5F).toLong();
}
visibilityAnimator.setDuration(duration)
visibilityAnimator.start()
mVisibilityAnimator = visibilityAnimator
}
private fun setVisibilityAmount(visibilityAmount: Float) {
mLinearVisibilityAmount = visibilityAmount
mVisibilityAmount = mVisibilityInterpolator.getInterpolation(
visibilityAmount)
handleAnimationFinished();
updateHideAmount()
}
private fun handleAnimationFinished() {
if (mLinearDozeAmount == 0.0f || mLinearVisibilityAmount == 0.0f) {
mEntrySetToClearWhenFinished.forEach { it.setHeadsUpAnimatingAway(false) }
mEntrySetToClearWhenFinished.clear()
}
}
fun getWakeUpHeight() : Float {
return mStackScroller.wakeUpHeight
}
private fun updateHideAmount() {
val linearAmount = Math.min(1.0f - mLinearVisibilityAmount, mLinearDozeAmount)
val amount = Math.min(1.0f - mVisibilityAmount, mDozeAmount)
mStackScroller.setHideAmount(linearAmount, amount)
notificationsFullyHidden = linearAmount == 1.0f;
}
private fun notifyAnimationStart(awake: Boolean) {
mStackScroller.notifyHideAnimationStart(!awake)
}
override fun onDozingChanged(isDozing: Boolean) {
if (isDozing) {
setNotificationsVisible(visible = false, animate = false, increaseSpeed = false)
}
}
/**
* Set the height how tall notifications are pulsing. This is only set whenever we are expanding
* from a pulse and determines how much the notifications are expanded.
*/
fun setPulseHeight(height: Float): Float {
val overflow = mStackScroller.setPulseHeight(height)
// no overflow for the bypass experience
return if (bypassController.bypassEnabled) 0.0f else overflow
}
override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
var animate = shouldAnimateVisibility()
if (!isHeadsUp) {
if (mLinearDozeAmount != 0.0f && mLinearVisibilityAmount != 0.0f) {
if (entry.isRowDismissed) {
// if we animate, we see the shelf briefly visible. Instead we fully animate
// the notification and its background out
animate = false
} else if (!wakingUp && !willWakeUp){
// TODO: look that this is done properly and not by anyone else
entry.setHeadsUpAnimatingAway(true)
mEntrySetToClearWhenFinished.add(entry)
}
}
} else if (mEntrySetToClearWhenFinished.contains(entry)) {
mEntrySetToClearWhenFinished.remove(entry)
entry.setHeadsUpAnimatingAway(false)
}
updateNotificationVisibility(animate, increaseSpeed = false)
}
private fun shouldAnimateVisibility() =
mDozeParameters.getAlwaysOn() && !mDozeParameters.getDisplayNeedsBlanking()
interface WakeUpListener {
/**
* Called whenever the notifications are fully hidden or shown
*/
@JvmDefault fun onFullyHiddenChanged(isFullyHidden: Boolean) {}
/**
* Called whenever the pulseExpansion changes
* @param expandingChanged if the user has started or stopped expanding
*/
@JvmDefault fun onPulseExpansionChanged(expandingChanged: Boolean) {}
}
}