1 /* <lambda>null2 * 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 18 19 import android.animation.Animator 20 import android.animation.AnimatorListenerAdapter 21 import android.animation.ObjectAnimator 22 import android.animation.ValueAnimator 23 import android.content.Context 24 import android.os.PowerManager 25 import android.os.PowerManager.WAKE_REASON_GESTURE 26 import android.os.SystemClock 27 import android.view.MotionEvent 28 import android.view.VelocityTracker 29 import android.view.ViewConfiguration 30 import com.android.systemui.Dependency 31 32 import com.android.systemui.Gefingerpoken 33 import com.android.systemui.Interpolators 34 import com.android.systemui.R 35 import com.android.systemui.plugins.FalsingManager 36 import com.android.systemui.plugins.statusbar.StatusBarStateController 37 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator 38 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 39 import com.android.systemui.statusbar.notification.row.ExpandableView 40 import com.android.systemui.statusbar.notification.stack.NotificationRoundnessManager 41 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout 42 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone 43 import com.android.systemui.statusbar.phone.KeyguardBypassController 44 import com.android.systemui.statusbar.phone.ShadeController 45 46 import javax.inject.Inject 47 import javax.inject.Singleton 48 import kotlin.math.max 49 50 /** 51 * A utility class to enable the downward swipe on when pulsing. 52 */ 53 @Singleton 54 class PulseExpansionHandler @Inject 55 constructor( 56 context: Context, 57 private val wakeUpCoordinator: NotificationWakeUpCoordinator, 58 private val bypassController: KeyguardBypassController, 59 private val headsUpManager: HeadsUpManagerPhone, 60 private val roundnessManager: NotificationRoundnessManager, 61 private val statusBarStateController: StatusBarStateController 62 ) : Gefingerpoken { 63 companion object { 64 private val RUBBERBAND_FACTOR_STATIC = 0.25f 65 private val SPRING_BACK_ANIMATION_LENGTH_MS = 375 66 } 67 private val mPowerManager: PowerManager? 68 private lateinit var shadeController: ShadeController 69 70 private val mMinDragDistance: Int 71 private var mInitialTouchX: Float = 0.0f 72 private var mInitialTouchY: Float = 0.0f 73 var isExpanding: Boolean = false 74 private set(value) { 75 val changed = field != value 76 field = value 77 bypassController.isPulseExpanding = value 78 if (changed) { 79 if (value) { 80 val topEntry = headsUpManager.topEntry 81 topEntry?.let { 82 roundnessManager.setTrackingHeadsUp(it.row) 83 } 84 } else { 85 roundnessManager.setTrackingHeadsUp(null) 86 if (!leavingLockscreen) { 87 bypassController.maybePerformPendingUnlock() 88 pulseExpandAbortListener?.run() 89 } 90 } 91 headsUpManager.unpinAll(true /* userUnPinned */) 92 } 93 } 94 var leavingLockscreen: Boolean = false 95 private set 96 private val mTouchSlop: Float 97 private lateinit var expansionCallback: ExpansionCallback 98 private lateinit var stackScroller: NotificationStackScrollLayout 99 private val mTemp2 = IntArray(2) 100 private var mDraggedFarEnough: Boolean = false 101 private var mStartingChild: ExpandableView? = null 102 private val mFalsingManager: FalsingManager 103 private var mPulsing: Boolean = false 104 var isWakingToShadeLocked: Boolean = false 105 private set 106 private var mEmptyDragAmount: Float = 0.0f 107 private var mWakeUpHeight: Float = 0.0f 108 private var mReachedWakeUpHeight: Boolean = false 109 private var velocityTracker: VelocityTracker? = null 110 111 private val isFalseTouch: Boolean 112 get() = mFalsingManager.isFalseTouch 113 var qsExpanded: Boolean = false 114 var pulseExpandAbortListener: Runnable? = null 115 var bouncerShowing: Boolean = false 116 117 init { 118 mMinDragDistance = context.resources.getDimensionPixelSize( 119 R.dimen.keyguard_drag_down_min_distance) 120 mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop.toFloat() 121 mFalsingManager = Dependency.get(FalsingManager::class.java) 122 mPowerManager = context.getSystemService(PowerManager::class.java) 123 } 124 125 override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 126 return maybeStartExpansion(event) 127 } 128 129 private fun maybeStartExpansion(event: MotionEvent): Boolean { 130 if (!wakeUpCoordinator.canShowPulsingHuns || qsExpanded || 131 bouncerShowing) { 132 return false 133 } 134 if (velocityTracker == null) { 135 velocityTracker = VelocityTracker.obtain() 136 } 137 velocityTracker!!.addMovement(event) 138 val x = event.x 139 val y = event.y 140 141 when (event.actionMasked) { 142 MotionEvent.ACTION_DOWN -> { 143 mDraggedFarEnough = false 144 isExpanding = false 145 leavingLockscreen = false 146 mStartingChild = null 147 mInitialTouchY = y 148 mInitialTouchX = x 149 } 150 151 MotionEvent.ACTION_MOVE -> { 152 val h = y - mInitialTouchY 153 if (h > mTouchSlop && h > Math.abs(x - mInitialTouchX)) { 154 mFalsingManager.onStartExpandingFromPulse() 155 isExpanding = true 156 captureStartingChild(mInitialTouchX, mInitialTouchY) 157 mInitialTouchY = y 158 mInitialTouchX = x 159 mWakeUpHeight = wakeUpCoordinator.getWakeUpHeight() 160 mReachedWakeUpHeight = false 161 return true 162 } 163 } 164 165 MotionEvent.ACTION_UP -> { 166 recycleVelocityTracker() 167 } 168 169 MotionEvent.ACTION_CANCEL -> { 170 recycleVelocityTracker() 171 } 172 } 173 return false 174 } 175 176 private fun recycleVelocityTracker() { 177 velocityTracker?.recycle() 178 velocityTracker = null 179 } 180 181 override fun onTouchEvent(event: MotionEvent): Boolean { 182 if (!isExpanding) { 183 return maybeStartExpansion(event) 184 } 185 velocityTracker!!.addMovement(event) 186 val y = event.y 187 188 val moveDistance = y - mInitialTouchY 189 when (event.actionMasked) { 190 MotionEvent.ACTION_MOVE -> updateExpansionHeight(moveDistance) 191 MotionEvent.ACTION_UP -> { 192 velocityTracker!!.computeCurrentVelocity(1000 /* units */) 193 val canExpand = moveDistance > 0 && velocityTracker!!.getYVelocity() > -1000 && 194 statusBarStateController.state != StatusBarState.SHADE 195 if (!mFalsingManager.isUnlockingDisabled && !isFalseTouch && canExpand) { 196 finishExpansion() 197 } else { 198 cancelExpansion() 199 } 200 recycleVelocityTracker() 201 } 202 MotionEvent.ACTION_CANCEL -> { 203 cancelExpansion() 204 recycleVelocityTracker() 205 } 206 } 207 return isExpanding 208 } 209 210 private fun finishExpansion() { 211 resetClock() 212 if (mStartingChild != null) { 213 setUserLocked(mStartingChild!!, false) 214 mStartingChild = null 215 } 216 if (shadeController.isDozing) { 217 isWakingToShadeLocked = true 218 wakeUpCoordinator.willWakeUp = true 219 mPowerManager!!.wakeUp(SystemClock.uptimeMillis(), WAKE_REASON_GESTURE, 220 "com.android.systemui:PULSEDRAG") 221 } 222 shadeController.goToLockedShade(mStartingChild) 223 leavingLockscreen = true 224 isExpanding = false 225 if (mStartingChild is ExpandableNotificationRow) { 226 val row = mStartingChild as ExpandableNotificationRow? 227 row!!.onExpandedByGesture(true /* userExpanded */) 228 } 229 } 230 231 private fun updateExpansionHeight(height: Float) { 232 var expansionHeight = max(height, 0.0f) 233 if (!mReachedWakeUpHeight && height > mWakeUpHeight) { 234 mReachedWakeUpHeight = true 235 } 236 if (mStartingChild != null) { 237 val child = mStartingChild!! 238 val newHeight = Math.min((child.collapsedHeight + expansionHeight).toInt(), 239 child.maxContentHeight) 240 child.actualHeight = newHeight 241 expansionHeight = max(newHeight.toFloat(), expansionHeight) 242 } else { 243 val target = if (mReachedWakeUpHeight) mWakeUpHeight else 0.0f 244 wakeUpCoordinator.setNotificationsVisibleForExpansion(height > target, 245 true /* animate */, 246 true /* increaseSpeed */) 247 expansionHeight = max(mWakeUpHeight, expansionHeight) 248 } 249 val emptyDragAmount = wakeUpCoordinator.setPulseHeight(expansionHeight) 250 setEmptyDragAmount(emptyDragAmount * RUBBERBAND_FACTOR_STATIC) 251 } 252 253 private fun captureStartingChild(x: Float, y: Float) { 254 if (mStartingChild == null && !bypassController.bypassEnabled) { 255 mStartingChild = findView(x, y) 256 if (mStartingChild != null) { 257 setUserLocked(mStartingChild!!, true) 258 } 259 } 260 } 261 262 private fun setEmptyDragAmount(amount: Float) { 263 mEmptyDragAmount = amount 264 expansionCallback.setEmptyDragAmount(amount) 265 } 266 267 private fun reset(child: ExpandableView) { 268 if (child.actualHeight == child.collapsedHeight) { 269 setUserLocked(child, false) 270 return 271 } 272 val anim = ObjectAnimator.ofInt(child, "actualHeight", 273 child.actualHeight, child.collapsedHeight) 274 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 275 anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong() 276 anim.addListener(object : AnimatorListenerAdapter() { 277 override fun onAnimationEnd(animation: Animator) { 278 setUserLocked(child, false) 279 } 280 }) 281 anim.start() 282 } 283 284 private fun setUserLocked(child: ExpandableView, userLocked: Boolean) { 285 if (child is ExpandableNotificationRow) { 286 child.isUserLocked = userLocked 287 } 288 } 289 290 private fun resetClock() { 291 val anim = ValueAnimator.ofFloat(mEmptyDragAmount, 0f) 292 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 293 anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS.toLong() 294 anim.addUpdateListener { animation -> setEmptyDragAmount(animation.animatedValue as Float) } 295 anim.start() 296 } 297 298 private fun cancelExpansion() { 299 isExpanding = false 300 mFalsingManager.onExpansionFromPulseStopped() 301 if (mStartingChild != null) { 302 reset(mStartingChild!!) 303 mStartingChild = null 304 } else { 305 resetClock() 306 } 307 wakeUpCoordinator.setNotificationsVisibleForExpansion(false /* visible */, 308 true /* animate */, 309 false /* increaseSpeed */) 310 } 311 312 private fun findView(x: Float, y: Float): ExpandableView? { 313 var totalX = x 314 var totalY = y 315 stackScroller.getLocationOnScreen(mTemp2) 316 totalX += mTemp2[0].toFloat() 317 totalY += mTemp2[1].toFloat() 318 val childAtRawPosition = stackScroller.getChildAtRawPosition(totalX, totalY) 319 return if (childAtRawPosition != null && childAtRawPosition.isContentExpandable) { 320 childAtRawPosition 321 } else null 322 } 323 324 fun setUp( 325 stackScroller: NotificationStackScrollLayout, 326 expansionCallback: ExpansionCallback, 327 shadeController: ShadeController 328 ) { 329 this.expansionCallback = expansionCallback 330 this.shadeController = shadeController 331 this.stackScroller = stackScroller 332 } 333 334 fun setPulsing(pulsing: Boolean) { 335 mPulsing = pulsing 336 } 337 338 fun onStartedWakingUp() { 339 isWakingToShadeLocked = false 340 } 341 342 interface ExpansionCallback { 343 fun setEmptyDragAmount(amount: Float) 344 } 345 } 346