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