• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

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