<lambda>null1 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
<lambda>null787 init {
788 updateResources(context)
789 }
790
updateResourcesnull791 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
onInterceptTouchEventnull799 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
onTouchEventnull833 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
captureStartingChildnull880 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
handleExpansionnull893 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
cancelChildExpansionnull914 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
stopDraggingnull937 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
findViewnull947 private fun findView(x: Float, y: Float): ExpandableView? {
948 return expandCallback.getChildAtRawPosition(x, y)
949 }
950 }
951