blob: 48d6de964b080a84aa543f98b211f851b2fb7e58 [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
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.os.PowerManager
import android.os.PowerManager.WAKE_REASON_GESTURE
import android.os.SystemClock
import android.view.MotionEvent
import android.view.VelocityTracker
import android.view.ViewConfiguration
import com.android.systemui.Dependency
import com.android.systemui.Gefingerpoken
import com.android.systemui.Interpolators
import com.android.systemui.R
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.phone.HeadsUpManagerPhone
import com.android.systemui.statusbar.phone.KeyguardBypassController
import com.android.systemui.statusbar.phone.ShadeController
import com.android.systemui.statusbar.policy.HeadsUpManager
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.math.max
/**
* A utility class to enable the downward swipe on when pulsing.
*/
@Singleton
class PulseExpansionHandler @Inject
constructor(context: Context,
private val wakeUpCoordinator: NotificationWakeUpCoordinator,
private val bypassController: KeyguardBypassController,
private val headsUpManager: HeadsUpManagerPhone,
private val roundnessManager: NotificationRoundnessManager) : Gefingerpoken {
companion object {
private val RUBBERBAND_FACTOR_STATIC = 0.25f
private val SPRING_BACK_ANIMATION_LENGTH_MS = 375
}
private val mPowerManager: PowerManager?
private lateinit var shadeController: ShadeController
private val mMinDragDistance: Int
private var mInitialTouchX: Float = 0.0f
private var mInitialTouchY: Float = 0.0f
var isExpanding: Boolean = false
private set(value) {
val changed = field != value
field = value
bypassController.isPulseExpanding = value
if (changed) {
if (value) {
val topEntry = headsUpManager.topEntry
topEntry?.let {
roundnessManager.setTrackingHeadsUp(it.row)
}
} else {
roundnessManager.setTrackingHeadsUp(null)
if (!leavingLockscreen) {
bypassController.maybePerformPendingUnlock()
pulseExpandAbortListener?.run()
}
}
headsUpManager.unpinAll(true /* userUnPinned */)
}
}
var leavingLockscreen: Boolean = false
private set
private val mTouchSlop: Float
private lateinit var expansionCallback: ExpansionCallback
private lateinit var stackScroller: NotificationStackScrollLayout
private val mTemp2 = IntArray(2)
private var mDraggedFarEnough: Boolean = false
private var mStartingChild: ExpandableView? = null
private val mFalsingManager: FalsingManager
private var mPulsing: Boolean = false
var isWakingToShadeLocked: Boolean = false
private set
private var mEmptyDragAmount: Float = 0.0f
private var mWakeUpHeight: Float = 0.0f
private var mReachedWakeUpHeight: Boolean = false
private var velocityTracker: VelocityTracker? = null
private val isFalseTouch: Boolean
get() = mFalsingManager.isFalseTouch
var qsExpanded: Boolean = false
var pulseExpandAbortListener: Runnable? = null
var bouncerShowing: Boolean = false
init {
mMinDragDistance = context.resources.getDimensionPixelSize(
R.dimen.keyguard_drag_down_min_distance)
mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat()
mFalsingManager = Dependency.get(FalsingManager::class.java)
mPowerManager = context.getSystemService(PowerManager::class.java)
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
return maybeStartExpansion(event)
}
private fun maybeStartExpansion(event: MotionEvent): Boolean {
if (!wakeUpCoordinator.canShowPulsingHuns || qsExpanded
|| bouncerShowing) {
return false
}
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain()
}
velocityTracker!!.addMovement(event)
val x = event.x
val y = event.y
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
mDraggedFarEnough = false
isExpanding = false
leavingLockscreen = false
mStartingChild = null
mInitialTouchY = y
mInitialTouchX = x
}
MotionEvent.ACTION_MOVE -> {
val h = y - mInitialTouchY
if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) {
mFalsingManager.onStartExpandingFromPulse()
isExpanding = true
captureStartingChild(mInitialTouchX, mInitialTouchY)
mInitialTouchY = y
mInitialTouchX = x
mWakeUpHeight = wakeUpCoordinator.getWakeUpHeight()
mReachedWakeUpHeight = false
return true
}
}
MotionEvent.ACTION_UP -> {
recycleVelocityTracker();
}
MotionEvent.ACTION_CANCEL -> {
recycleVelocityTracker();
}
}
return false
}
private fun recycleVelocityTracker() {
velocityTracker?.recycle();
velocityTracker = null
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isExpanding) {
return maybeStartExpansion(event)
}
velocityTracker!!.addMovement(event)
val y = event.y
val moveDistance = y - mInitialTouchY
when (event.actionMasked) {
MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance)
MotionEvent.ACTION_UP -> {
velocityTracker!!.computeCurrentVelocity(1000 /* units */)
val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000
if (!mFalsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) {
finishExpansion()
} else {
cancelExpansion()
}
recycleVelocityTracker()
}
MotionEvent.ACTION_CANCEL -> {
cancelExpansion()
recycleVelocityTracker()
}
}
return isExpanding
}
private fun finishExpansion() {
resetClock()
if (mStartingChild != null) {
setUserLocked(mStartingChild!!, false)
mStartingChild = null
}
if (shadeController.isDozing) {
isWakingToShadeLocked = true
wakeUpCoordinator.willWakeUp = true
mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE,
"com.android.systemui:PULSEDRAG")
}
shadeController.goToLockedShade(mStartingChild)
leavingLockscreen = true;
isExpanding = false
if (mStartingChild is ExpandableNotificationRow) {
val row = mStartingChild as ExpandableNotificationRow?
row!!.onExpandedByGesture(true /* userExpanded */)
}
}
private fun updateExpansionHeight(height: Float) {
var expansionHeight = max(height, 0.0f)
if (!mReachedWakeUpHeight && height > mWakeUpHeight) {
mReachedWakeUpHeight = true;
}
if (mStartingChild != null) {
val child = mStartingChild!!
val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(),
child.maxContentHeight)
child.actualHeight = newHeight
expansionHeight = max(newHeight.toFloat(), expansionHeight)
} else {
val target = if (mReachedWakeUpHeight) mWakeUpHeight else 0.0f
wakeUpCoordinator.setNotificationsVisibleForExpansion(height > target,
true /* animate */,
true /* increaseSpeed */)
expansionHeight = max(mWakeUpHeight, expansionHeight)
}
val emptyDragAmount = wakeUpCoordinator.setPulseHeight(expansionHeight)
setEmptyDragAmount(emptyDragAmount * RUBBERBAND_FACTOR_STATIC)
}
private fun captureStartingChild(x: Float, y: Float) {
if (mStartingChild == null && !bypassController.bypassEnabled) {
mStartingChild = findView(x, y)
if (mStartingChild != null) {
setUserLocked(mStartingChild!!, true)
}
}
}
private fun setEmptyDragAmount(amount: Float) {
mEmptyDragAmount = amount
expansionCallback.setEmptyDragAmount(amount)
}
private fun reset(child: ExpandableView) {
if (child.actualHeight == child.collapsedHeight) {
setUserLocked(child, false)
return
}
val anim = ObjectAnimator.ofInt(child, "actualHeight",
child.actualHeight, child.collapsedHeight)
anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
anim.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
setUserLocked(child, false)
}
})
anim.start()
}
private fun setUserLocked(child: ExpandableView, userLocked: Boolean) {
if (child is ExpandableNotificationRow) {
child.isUserLocked = userLocked
}
}
private fun resetClock() {
val anim = ValueAnimator.ofFloat(mEmptyDragAmount, 0f)
anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong()
anim.addUpdateListener { animation -> setEmptyDragAmount(animation.animatedValue as Float) }
anim.start()
}
private fun cancelExpansion() {
isExpanding = false
mFalsingManager.onExpansionFromPulseStopped()
if (mStartingChild != null) {
reset(mStartingChild!!)
mStartingChild = null
} else {
resetClock()
}
wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */,
true /* animate */,
false /* increaseSpeed */)
}
private fun findView(x: Float, y: Float): ExpandableView? {
var totalX = x
var totalY = y
stackScroller.getLocationOnScreen(mTemp2)
totalX += mTemp2[0].toFloat()
totalY += mTemp2[1].toFloat()
val childAtRawPosition = stackScroller.getChildAtRawPosition(totalX, totalY)
return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) {
childAtRawPosition
} else null
}
fun setUp(stackScroller: NotificationStackScrollLayout,
expansionCallback: ExpansionCallback,
shadeController: ShadeController) {
this.expansionCallback = expansionCallback
this.shadeController = shadeController
this.stackScroller = stackScroller
}
fun setPulsing(pulsing: Boolean) {
mPulsing = pulsing
}
fun onStartedWakingUp() {
isWakingToShadeLocked = false
}
interface ExpansionCallback {
fun setEmptyDragAmount(amount: Float)
}
}