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