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