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