1 package com.android.systemui.statusbar 2 3 import android.animation.Animator 4 import android.animation.AnimatorListenerAdapter 5 import android.animation.ObjectAnimator 6 import android.animation.ValueAnimator 7 import android.content.Context 8 import android.content.res.Configuration 9 import android.os.SystemClock 10 import android.util.IndentingPrintWriter 11 import android.util.MathUtils 12 import android.view.MotionEvent 13 import android.view.View 14 import android.view.ViewConfiguration 15 import androidx.annotation.VisibleForTesting 16 import com.android.systemui.Dumpable 17 import com.android.systemui.ExpandHelper 18 import com.android.systemui.Gefingerpoken 19 import com.android.systemui.R 20 import com.android.systemui.animation.Interpolators 21 import com.android.systemui.biometrics.UdfpsKeyguardViewController 22 import com.android.systemui.classifier.Classifier 23 import com.android.systemui.classifier.FalsingCollector 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dump.DumpManager 26 import com.android.systemui.keyguard.WakefulnessLifecycle 27 import com.android.systemui.media.MediaHierarchyManager 28 import com.android.systemui.plugins.ActivityStarter.OnDismissAction 29 import com.android.systemui.plugins.FalsingManager 30 import com.android.systemui.plugins.qs.QS 31 import com.android.systemui.plugins.statusbar.StatusBarStateController 32 import com.android.systemui.statusbar.notification.collection.NotificationEntry 33 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow 34 import com.android.systemui.statusbar.notification.row.ExpandableView 35 import com.android.systemui.statusbar.notification.stack.AmbientState 36 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController 37 import com.android.systemui.statusbar.phone.KeyguardBypassController 38 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger 39 import com.android.systemui.statusbar.phone.NotificationPanelViewController 40 import com.android.systemui.statusbar.phone.ScrimController 41 import com.android.systemui.statusbar.phone.StatusBar 42 import com.android.systemui.statusbar.policy.ConfigurationController 43 import com.android.systemui.util.Utils 44 import java.io.FileDescriptor 45 import java.io.PrintWriter 46 import javax.inject.Inject 47 48 private const val SPRING_BACK_ANIMATION_LENGTH_MS = 375L 49 private const val RUBBERBAND_FACTOR_STATIC = 0.15f 50 private const val RUBBERBAND_FACTOR_EXPANDABLE = 0.5f 51 52 /** 53 * A class that controls the lockscreen to shade transition 54 */ 55 @SysUISingleton 56 class LockscreenShadeTransitionController @Inject constructor( 57 private val statusBarStateController: SysuiStatusBarStateController, 58 private val logger: LSShadeTransitionLogger, 59 private val keyguardBypassController: KeyguardBypassController, 60 private val lockScreenUserManager: NotificationLockscreenUserManager, 61 private val falsingCollector: FalsingCollector, 62 private val ambientState: AmbientState, 63 private val mediaHierarchyManager: MediaHierarchyManager, 64 private val scrimController: ScrimController, 65 private val depthController: NotificationShadeDepthController, 66 private val context: Context, 67 wakefulnessLifecycle: WakefulnessLifecycle, 68 configurationController: ConfigurationController, 69 falsingManager: FalsingManager, 70 dumpManager: DumpManager, 71 ) : Dumpable { 72 private var pulseHeight: Float = 0f 73 private var useSplitShade: Boolean = false 74 private lateinit var nsslController: NotificationStackScrollLayoutController 75 lateinit var notificationPanelController: NotificationPanelViewController 76 lateinit var statusbar: StatusBar 77 lateinit var qS: QS 78 79 /** 80 * A handler that handles the next keyguard dismiss animation. 81 */ 82 private var animationHandlerOnKeyguardDismiss: ((Long) -> Unit)? = null 83 84 /** 85 * The entry that was just dragged down on. 86 */ 87 private var draggedDownEntry: NotificationEntry? = null 88 89 /** 90 * The current animator if any 91 */ 92 @VisibleForTesting 93 internal var dragDownAnimator: ValueAnimator? = null 94 95 /** 96 * The current pulse height animator if any 97 */ 98 @VisibleForTesting 99 internal var pulseHeightAnimator: ValueAnimator? = null 100 101 /** 102 * Distance that the full shade transition takes in order for scrim to fully transition to 103 * the shade (in alpha) 104 */ 105 private var scrimTransitionDistance = 0 106 107 /** 108 * Distance that the full transition takes in order for us to fully transition to the shade 109 */ 110 private var fullTransitionDistance = 0 111 112 /** 113 * Flag to make sure that the dragDownAmount is applied to the listeners even when in the 114 * locked down shade. 115 */ 116 private var forceApplyAmount = false 117 118 /** 119 * A flag to suppress the default animation when unlocking in the locked down shade. 120 */ 121 private var nextHideKeyguardNeedsNoAnimation = false 122 123 /** 124 * Are we currently waking up to the shade locked 125 */ 126 var isWakingToShadeLocked: Boolean = false 127 private set 128 129 /** 130 * The distance until we're showing the notifications when pulsing 131 */ 132 val distanceUntilShowingPulsingNotifications 133 get() = scrimTransitionDistance 134 135 /** 136 * The udfpsKeyguardViewController if it exists. 137 */ 138 var udfpsKeyguardViewController: UdfpsKeyguardViewController? = null 139 140 /** 141 * The touch helper responsible for the drag down animation. 142 */ 143 val touchHelper = DragDownHelper(falsingManager, falsingCollector, this, context) 144 145 init { 146 updateResources() 147 configurationController.addCallback(object : ConfigurationController.ConfigurationListener { 148 override fun onConfigChanged(newConfig: Configuration?) { 149 updateResources() 150 touchHelper.updateResources(context) 151 } 152 }) 153 dumpManager.registerDumpable(this) 154 statusBarStateController.addCallback(object : StatusBarStateController.StateListener { 155 override fun onExpandedChanged(isExpanded: Boolean) { 156 // safeguard: When the panel is fully collapsed, let's make sure to reset. 157 // See b/198098523 158 if (!isExpanded) { 159 if (dragDownAmount != 0f && dragDownAnimator?.isRunning != true) { 160 logger.logDragDownAmountResetWhenFullyCollapsed() 161 dragDownAmount = 0f 162 } 163 if (pulseHeight != 0f && pulseHeightAnimator?.isRunning != true) { 164 logger.logPulseHeightNotResetWhenFullyCollapsed() 165 setPulseHeight(0f, animate = false) 166 } 167 } 168 } 169 }) 170 wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer { 171 override fun onPostFinishedWakingUp() { 172 // when finishing waking up, the UnlockedScreenOffAnimation has another attempt 173 // to reset keyguard. Let's do it in post 174 isWakingToShadeLocked = false 175 } 176 }) 177 } 178 179 private fun updateResources() { 180 scrimTransitionDistance = context.resources.getDimensionPixelSize( 181 R.dimen.lockscreen_shade_scrim_transition_distance) 182 fullTransitionDistance = context.resources.getDimensionPixelSize( 183 R.dimen.lockscreen_shade_qs_transition_distance) 184 useSplitShade = Utils.shouldUseSplitNotificationShade(context.resources) 185 } 186 187 fun setStackScroller(nsslController: NotificationStackScrollLayoutController) { 188 this.nsslController = nsslController 189 touchHelper.host = nsslController.view 190 touchHelper.expandCallback = nsslController.expandHelperCallback 191 } 192 193 /** 194 * Initialize the shelf controller such that clicks on it will expand the shade 195 */ 196 fun bindController(notificationShelfController: NotificationShelfController) { 197 // Bind the click listener of the shelf to go to the full shade 198 notificationShelfController.setOnClickListener { 199 if (statusBarStateController.state == StatusBarState.KEYGUARD) { 200 statusbar.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK") 201 goToLockedShade(it) 202 } 203 } 204 } 205 206 /** 207 * @return true if the interaction is accepted, false if it should be cancelled 208 */ 209 internal fun canDragDown(): Boolean { 210 return (statusBarStateController.state == StatusBarState.KEYGUARD || 211 nsslController.isInLockedDownShade()) && 212 (qS.isFullyCollapsed || useSplitShade) 213 } 214 215 /** 216 * Called by the touch helper when when a gesture has completed all the way and released. 217 */ 218 internal fun onDraggedDown(startingChild: View?, dragLengthY: Int) { 219 if (canDragDown()) { 220 val cancelRunnable = Runnable { 221 logger.logGoingToLockedShadeAborted() 222 setDragDownAmountAnimated(0f) 223 } 224 if (nsslController.isInLockedDownShade()) { 225 logger.logDraggedDownLockDownShade(startingChild) 226 statusBarStateController.setLeaveOpenOnKeyguardHide(true) 227 statusbar.dismissKeyguardThenExecute(OnDismissAction { 228 nextHideKeyguardNeedsNoAnimation = true 229 false 230 }, cancelRunnable, false /* afterKeyguardGone */) 231 } else { 232 logger.logDraggedDown(startingChild, dragLengthY) 233 if (!ambientState.isDozing() || startingChild != null) { 234 // go to locked shade while animating the drag down amount from its current 235 // value 236 val animationHandler = { delay: Long -> 237 if (startingChild is ExpandableNotificationRow) { 238 startingChild.onExpandedByGesture( 239 true /* drag down is always an open */) 240 } 241 notificationPanelController.animateToFullShade(delay) 242 notificationPanelController.setTransitionToFullShadeAmount(0f, 243 true /* animated */, delay) 244 245 // Let's reset ourselves, ready for the next animation 246 247 // changing to shade locked will make isInLockDownShade true, so let's 248 // override that 249 forceApplyAmount = true 250 // Reset the behavior. At this point the animation is already started 251 dragDownAmount = 0f 252 forceApplyAmount = false 253 } 254 goToLockedShadeInternal(startingChild, animationHandler, cancelRunnable) 255 } 256 } 257 } else { 258 logger.logUnSuccessfulDragDown(startingChild) 259 setDragDownAmountAnimated(0f) 260 } 261 } 262 263 /** 264 * Called by the touch helper when the drag down was aborted and should be reset. 265 */ 266 internal fun onDragDownReset() { 267 logger.logDragDownAborted() 268 nsslController.setDimmed(true /* dimmed */, true /* animated */) 269 nsslController.resetScrollPosition() 270 nsslController.resetCheckSnoozeLeavebehind() 271 setDragDownAmountAnimated(0f) 272 } 273 274 /** 275 * The user has dragged either above or below the threshold which changes the dimmed state. 276 * @param above whether they dragged above it 277 */ 278 internal fun onCrossedThreshold(above: Boolean) { 279 nsslController.setDimmed(!above /* dimmed */, true /* animate */) 280 } 281 282 /** 283 * Called by the touch helper when the drag down was started 284 */ 285 internal fun onDragDownStarted(startingChild: ExpandableView?) { 286 logger.logDragDownStarted(startingChild) 287 nsslController.cancelLongPress() 288 nsslController.checkSnoozeLeavebehind() 289 dragDownAnimator?.apply { 290 if (isRunning) { 291 logger.logAnimationCancelled(isPulse = false) 292 cancel() 293 } 294 } 295 } 296 297 /** 298 * Do we need a falsing check currently? 299 */ 300 internal val isFalsingCheckNeeded: Boolean 301 get() = statusBarStateController.state == StatusBarState.KEYGUARD 302 303 /** 304 * Is dragging down enabled on a given view 305 * @param view The view to check or `null` to check if it's enabled at all 306 */ 307 internal fun isDragDownEnabledForView(view: ExpandableView?): Boolean { 308 if (isDragDownAnywhereEnabled) { 309 return true 310 } 311 if (nsslController.isInLockedDownShade()) { 312 if (view == null) { 313 // Dragging down is allowed in general 314 return true 315 } 316 if (view is ExpandableNotificationRow) { 317 // Only drag down on sensitive views, otherwise the ExpandHelper will take this 318 return view.entry.isSensitive 319 } 320 } 321 return false 322 } 323 324 /** 325 * @return if drag down is enabled anywhere, not just on selected views. 326 */ 327 internal val isDragDownAnywhereEnabled: Boolean 328 get() = (statusBarStateController.getState() == StatusBarState.KEYGUARD && 329 !keyguardBypassController.bypassEnabled && 330 (qS.isFullyCollapsed || useSplitShade)) 331 332 /** 333 * The amount in pixels that the user has dragged down. 334 */ 335 internal var dragDownAmount = 0f 336 set(value) { 337 if (field != value || forceApplyAmount) { 338 field = value 339 if (!nsslController.isInLockedDownShade() || field == 0f || forceApplyAmount) { 340 nsslController.setTransitionToFullShadeAmount(field) 341 notificationPanelController.setTransitionToFullShadeAmount(field, 342 false /* animate */, 0 /* delay */) 343 val progress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance) 344 qS.setTransitionToFullShadeAmount(field, progress) 345 // TODO: appear media also in split shade 346 val mediaAmount = if (useSplitShade) 0f else field 347 mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount) 348 transitionToShadeAmountCommon(field) 349 } 350 } 351 } 352 353 private fun transitionToShadeAmountCommon(dragDownAmount: Float) { 354 val scrimProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance) 355 scrimController.setTransitionToFullShadeProgress(scrimProgress) 356 // Fade out all content only visible on the lockscreen 357 notificationPanelController.setKeyguardOnlyContentAlpha(1.0f - scrimProgress) 358 depthController.transitionToFullShadeProgress = scrimProgress 359 udfpsKeyguardViewController?.setTransitionToFullShadeProgress(scrimProgress) 360 } 361 362 private fun setDragDownAmountAnimated( 363 target: Float, 364 delay: Long = 0, 365 endlistener: (() -> Unit)? = null 366 ) { 367 logger.logDragDownAnimation(target) 368 val dragDownAnimator = ValueAnimator.ofFloat(dragDownAmount, target) 369 dragDownAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN 370 dragDownAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS 371 dragDownAnimator.addUpdateListener { animation: ValueAnimator -> 372 dragDownAmount = animation.animatedValue as Float 373 } 374 if (delay > 0) { 375 dragDownAnimator.startDelay = delay 376 } 377 if (endlistener != null) { 378 dragDownAnimator.addListener(object : AnimatorListenerAdapter() { 379 override fun onAnimationEnd(animation: Animator?) { 380 endlistener.invoke() 381 } 382 }) 383 } 384 dragDownAnimator.start() 385 this.dragDownAnimator = dragDownAnimator 386 } 387 388 /** 389 * Animate appear the drag down amount. 390 */ 391 private fun animateAppear(delay: Long = 0) { 392 // changing to shade locked will make isInLockDownShade true, so let's override 393 // that 394 forceApplyAmount = true 395 396 // we set the value initially to 1 pixel, since that will make sure we're 397 // transitioning to the full shade. this is important to avoid flickering, 398 // as the below animation only starts once the shade is unlocked, which can 399 // be a couple of frames later. if we're setting it to 0, it will use the 400 // default inset and therefore flicker 401 dragDownAmount = 1f 402 setDragDownAmountAnimated(fullTransitionDistance.toFloat(), delay = delay) { 403 // End listener: 404 // Reset 405 dragDownAmount = 0f 406 forceApplyAmount = false 407 } 408 } 409 410 /** 411 * Ask this controller to go to the locked shade, changing the state change and doing 412 * an animation, where the qs appears from 0 from the top 413 * 414 * If secure with redaction: Show bouncer, go to unlocked shade. 415 * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED]. 416 * 417 * @param expandView The view to expand after going to the shade 418 * @param needsQSAnimation if this needs the quick settings to slide in from the top or if 419 * that's already handled separately 420 */ 421 @JvmOverloads 422 fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) { 423 val isKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD 424 logger.logTryGoToLockedShade(isKeyguard) 425 if (isKeyguard) { 426 val animationHandler: ((Long) -> Unit)? 427 if (needsQSAnimation) { 428 // Let's use the default animation 429 animationHandler = null 430 } else { 431 // Let's only animate notifications 432 animationHandler = { delay: Long -> 433 notificationPanelController.animateToFullShade(delay) 434 } 435 } 436 goToLockedShadeInternal(expandedView, animationHandler, 437 cancelAction = null) 438 } 439 } 440 441 /** 442 * If secure with redaction: Show bouncer, go to unlocked shade. 443 * 444 * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED]. 445 * 446 * @param expandView The view to expand after going to the shade. 447 * @param animationHandler The handler which performs the go to full shade animation. If null, 448 * the default handler will do the animation, otherwise the caller is 449 * responsible for the animation. The input value is a Long for the 450 * delay for the animation. 451 * @param cancelAction The runnable to invoke when the transition is aborted. This happens if 452 * the user goes to the bouncer and goes back. 453 */ 454 private fun goToLockedShadeInternal( 455 expandView: View?, 456 animationHandler: ((Long) -> Unit)? = null, 457 cancelAction: Runnable? = null 458 ) { 459 if (statusbar.isShadeDisabled) { 460 cancelAction?.run() 461 logger.logShadeDisabledOnGoToLockedShade() 462 return 463 } 464 var userId: Int = lockScreenUserManager.getCurrentUserId() 465 var entry: NotificationEntry? = null 466 if (expandView is ExpandableNotificationRow) { 467 entry = expandView.entry 468 entry.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */) 469 // Indicate that the group expansion is changing at this time -- this way the group 470 // and children backgrounds / divider animations will look correct. 471 entry.setGroupExpansionChanging(true) 472 userId = entry.sbn.userId 473 } 474 var fullShadeNeedsBouncer = (!lockScreenUserManager.userAllowsPrivateNotificationsInPublic( 475 lockScreenUserManager.getCurrentUserId()) || 476 !lockScreenUserManager.shouldShowLockscreenNotifications() || 477 falsingCollector.shouldEnforceBouncer()) 478 if (keyguardBypassController.bypassEnabled) { 479 fullShadeNeedsBouncer = false 480 } 481 if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) { 482 statusBarStateController.setLeaveOpenOnKeyguardHide(true) 483 var onDismissAction: OnDismissAction? = null 484 if (animationHandler != null) { 485 onDismissAction = OnDismissAction { 486 // We're waiting on keyguard to hide before triggering the action, 487 // as that will make the animation work properly 488 animationHandlerOnKeyguardDismiss = animationHandler 489 false 490 } 491 } 492 val cancelHandler = Runnable { 493 draggedDownEntry?.apply { 494 setUserLocked(false) 495 notifyHeightChanged(false /* needsAnimation */) 496 draggedDownEntry = null 497 } 498 cancelAction?.run() 499 } 500 logger.logShowBouncerOnGoToLockedShade() 501 statusbar.showBouncerWithDimissAndCancelIfKeyguard(onDismissAction, cancelHandler) 502 draggedDownEntry = entry 503 } else { 504 logger.logGoingToLockedShade(animationHandler != null) 505 if (statusBarStateController.isDozing) { 506 // Make sure we don't go back to keyguard immediately again after waking up 507 isWakingToShadeLocked = true 508 } 509 statusBarStateController.setState(StatusBarState.SHADE_LOCKED) 510 // This call needs to be after updating the shade state since otherwise 511 // the scrimstate resets too early 512 if (animationHandler != null) { 513 animationHandler.invoke(0 /* delay */) 514 } else { 515 performDefaultGoToFullShadeAnimation(0) 516 } 517 } 518 } 519 520 /** 521 * Notify this handler that the keyguard was just dismissed and that a animation to 522 * the full shade should happen. 523 * 524 * @param delay the delay to do the animation with 525 * @param previousState which state were we in when we hid the keyguard? 526 */ 527 fun onHideKeyguard(delay: Long, previousState: Int) { 528 logger.logOnHideKeyguard() 529 if (animationHandlerOnKeyguardDismiss != null) { 530 animationHandlerOnKeyguardDismiss!!.invoke(delay) 531 animationHandlerOnKeyguardDismiss = null 532 } else { 533 if (nextHideKeyguardNeedsNoAnimation) { 534 nextHideKeyguardNeedsNoAnimation = false 535 } else if (previousState != StatusBarState.SHADE_LOCKED) { 536 // No animation necessary if we already were in the shade locked! 537 performDefaultGoToFullShadeAnimation(delay) 538 } 539 } 540 draggedDownEntry?.apply { 541 setUserLocked(false) 542 draggedDownEntry = null 543 } 544 } 545 546 /** 547 * Perform the default appear animation when going to the full shade. This is called when 548 * not triggered by gestures, e.g. when clicking on the shelf or expand button. 549 */ 550 private fun performDefaultGoToFullShadeAnimation(delay: Long) { 551 logger.logDefaultGoToFullShadeAnimation(delay) 552 notificationPanelController.animateToFullShade(delay) 553 animateAppear(delay) 554 } 555 556 // 557 // PULSE EXPANSION 558 // 559 560 /** 561 * Set the height how tall notifications are pulsing. This is only set whenever we are expanding 562 * from a pulse and determines how much the notifications are expanded. 563 */ 564 fun setPulseHeight(height: Float, animate: Boolean = false) { 565 if (animate) { 566 val pulseHeightAnimator = ValueAnimator.ofFloat(pulseHeight, height) 567 pulseHeightAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN 568 pulseHeightAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS 569 pulseHeightAnimator.addUpdateListener { animation: ValueAnimator -> 570 setPulseHeight(animation.animatedValue as Float) 571 } 572 pulseHeightAnimator.start() 573 this.pulseHeightAnimator = pulseHeightAnimator 574 } else { 575 pulseHeight = height 576 val overflow = nsslController.setPulseHeight(height) 577 notificationPanelController.setOverStrechAmount(overflow) 578 val transitionHeight = if (keyguardBypassController.bypassEnabled) height else 0.0f 579 transitionToShadeAmountCommon(transitionHeight) 580 } 581 } 582 583 /** 584 * Finish the pulse animation when the touch interaction finishes 585 * @param cancelled was the interaction cancelled and this is a reset? 586 */ 587 fun finishPulseAnimation(cancelled: Boolean) { 588 logger.logPulseExpansionFinished(cancelled) 589 if (cancelled) { 590 setPulseHeight(0f, animate = true) 591 } else { 592 notificationPanelController.onPulseExpansionFinished() 593 setPulseHeight(0f, animate = false) 594 } 595 } 596 597 /** 598 * Notify this class that a pulse expansion is starting 599 */ 600 fun onPulseExpansionStarted() { 601 logger.logPulseExpansionStarted() 602 pulseHeightAnimator?.apply { 603 if (isRunning) { 604 logger.logAnimationCancelled(isPulse = true) 605 cancel() 606 } 607 } 608 } 609 610 override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { 611 IndentingPrintWriter(pw, " ").let { 612 it.println("LSShadeTransitionController:") 613 it.increaseIndent() 614 it.println("pulseHeight: $pulseHeight") 615 it.println("useSplitShade: $useSplitShade") 616 it.println("dragDownAmount: $dragDownAmount") 617 it.println("isDragDownAnywhereEnabled: $isDragDownAnywhereEnabled") 618 it.println("isFalsingCheckNeeded: $isFalsingCheckNeeded") 619 it.println("isWakingToShadeLocked: $isWakingToShadeLocked") 620 it.println("hasPendingHandlerOnKeyguardDismiss: " + 621 "${animationHandlerOnKeyguardDismiss != null}") 622 } 623 } 624 } 625 626 /** 627 * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand 628 * the notification where the drag started. 629 */ 630 class DragDownHelper( 631 private val falsingManager: FalsingManager, 632 private val falsingCollector: FalsingCollector, 633 private val dragDownCallback: LockscreenShadeTransitionController, 634 context: Context 635 ) : Gefingerpoken { 636 637 private var dragDownAmountOnStart = 0.0f 638 lateinit var expandCallback: ExpandHelper.Callback 639 lateinit var host: View 640 641 private var minDragDistance = 0 642 private var initialTouchX = 0f 643 private var initialTouchY = 0f 644 private var touchSlop = 0f 645 private var slopMultiplier = 0f 646 private val temp2 = IntArray(2) 647 private var draggedFarEnough = false 648 private var startingChild: ExpandableView? = null 649 private var lastHeight = 0f 650 var isDraggingDown = false 651 private set 652 653 private val isFalseTouch: Boolean 654 get() { 655 return if (!dragDownCallback.isFalsingCheckNeeded) { 656 false 657 } else { 658 falsingManager.isFalseTouch(Classifier.NOTIFICATION_DRAG_DOWN) || !draggedFarEnough 659 } 660 } 661 662 val isDragDownEnabled: Boolean 663 get() = dragDownCallback.isDragDownEnabledForView(null) 664 665 init { 666 updateResources(context) 667 } 668 669 fun updateResources(context: Context) { 670 minDragDistance = context.resources.getDimensionPixelSize( 671 R.dimen.keyguard_drag_down_min_distance) 672 val configuration = ViewConfiguration.get(context) 673 touchSlop = configuration.scaledTouchSlop.toFloat() 674 slopMultiplier = configuration.scaledAmbiguousGestureMultiplier 675 } 676 677 override fun onInterceptTouchEvent(event: MotionEvent): Boolean { 678 val x = event.x 679 val y = event.y 680 when (event.actionMasked) { 681 MotionEvent.ACTION_DOWN -> { 682 draggedFarEnough = false 683 isDraggingDown = false 684 startingChild = null 685 initialTouchY = y 686 initialTouchX = x 687 } 688 MotionEvent.ACTION_MOVE -> { 689 val h = y - initialTouchY 690 // Adjust the touch slop if another gesture may be being performed. 691 val touchSlop = if (event.classification 692 == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE) 693 touchSlop * slopMultiplier 694 else 695 touchSlop 696 if (h > touchSlop && h > Math.abs(x - initialTouchX)) { 697 falsingCollector.onNotificationStartDraggingDown() 698 isDraggingDown = true 699 captureStartingChild(initialTouchX, initialTouchY) 700 initialTouchY = y 701 initialTouchX = x 702 dragDownCallback.onDragDownStarted(startingChild) 703 dragDownAmountOnStart = dragDownCallback.dragDownAmount 704 return startingChild != null || dragDownCallback.isDragDownAnywhereEnabled 705 } 706 } 707 } 708 return false 709 } 710 711 override fun onTouchEvent(event: MotionEvent): Boolean { 712 if (!isDraggingDown) { 713 return false 714 } 715 val x = event.x 716 val y = event.y 717 when (event.actionMasked) { 718 MotionEvent.ACTION_MOVE -> { 719 lastHeight = y - initialTouchY 720 captureStartingChild(initialTouchX, initialTouchY) 721 dragDownCallback.dragDownAmount = lastHeight + dragDownAmountOnStart 722 if (startingChild != null) { 723 handleExpansion(lastHeight, startingChild!!) 724 } 725 if (lastHeight > minDragDistance) { 726 if (!draggedFarEnough) { 727 draggedFarEnough = true 728 dragDownCallback.onCrossedThreshold(true) 729 } 730 } else { 731 if (draggedFarEnough) { 732 draggedFarEnough = false 733 dragDownCallback.onCrossedThreshold(false) 734 } 735 } 736 return true 737 } 738 MotionEvent.ACTION_UP -> if (!falsingManager.isUnlockingDisabled && !isFalseTouch && 739 dragDownCallback.canDragDown()) { 740 dragDownCallback.onDraggedDown(startingChild, (y - initialTouchY).toInt()) 741 if (startingChild != null) { 742 expandCallback.setUserLockedChild(startingChild, false) 743 startingChild = null 744 } 745 isDraggingDown = false 746 } else { 747 stopDragging() 748 return false 749 } 750 MotionEvent.ACTION_CANCEL -> { 751 stopDragging() 752 return false 753 } 754 } 755 return false 756 } 757 758 private fun captureStartingChild(x: Float, y: Float) { 759 if (startingChild == null) { 760 startingChild = findView(x, y) 761 if (startingChild != null) { 762 if (dragDownCallback.isDragDownEnabledForView(startingChild)) { 763 expandCallback.setUserLockedChild(startingChild, true) 764 } else { 765 startingChild = null 766 } 767 } 768 } 769 } 770 771 private fun handleExpansion(heightDelta: Float, child: ExpandableView) { 772 var hDelta = heightDelta 773 if (hDelta < 0) { 774 hDelta = 0f 775 } 776 val expandable = child.isContentExpandable 777 val rubberbandFactor = if (expandable) { 778 RUBBERBAND_FACTOR_EXPANDABLE 779 } else { 780 RUBBERBAND_FACTOR_STATIC 781 } 782 var rubberband = hDelta * rubberbandFactor 783 if (expandable && rubberband + child.collapsedHeight > child.maxContentHeight) { 784 var overshoot = rubberband + child.collapsedHeight - child.maxContentHeight 785 overshoot *= 1 - RUBBERBAND_FACTOR_STATIC 786 rubberband -= overshoot 787 } 788 child.actualHeight = (child.collapsedHeight + rubberband).toInt() 789 } 790 791 private fun cancelChildExpansion(child: ExpandableView) { 792 if (child.actualHeight == child.collapsedHeight) { 793 expandCallback.setUserLockedChild(child, false) 794 return 795 } 796 val anim = ObjectAnimator.ofInt(child, "actualHeight", 797 child.actualHeight, child.collapsedHeight) 798 anim.interpolator = Interpolators.FAST_OUT_SLOW_IN 799 anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS 800 anim.addListener(object : AnimatorListenerAdapter() { 801 override fun onAnimationEnd(animation: Animator) { 802 expandCallback.setUserLockedChild(child, false) 803 } 804 }) 805 anim.start() 806 } 807 808 private fun stopDragging() { 809 falsingCollector.onNotificationStopDraggingDown() 810 if (startingChild != null) { 811 cancelChildExpansion(startingChild!!) 812 startingChild = null 813 } 814 isDraggingDown = false 815 dragDownCallback.onDragDownReset() 816 } 817 818 private fun findView(x: Float, y: Float): ExpandableView? { 819 host.getLocationOnScreen(temp2) 820 return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1]) 821 } 822 }