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