1 package com.android.systemui.statusbar
2 
3 import android.animation.Animator
4 import android.animation.AnimatorListenerAdapter
5 import android.animation.ObjectAnimator
6 import android.animation.ValueAnimator
7 import android.content.Context
8 import android.content.res.Configuration
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.VisibleForTesting
16 import com.android.systemui.Dumpable
17 import com.android.systemui.ExpandHelper
18 import com.android.systemui.Gefingerpoken
19 import com.android.systemui.R
20 import com.android.systemui.animation.Interpolators
21 import com.android.systemui.biometrics.UdfpsKeyguardViewController
22 import com.android.systemui.classifier.Classifier
23 import com.android.systemui.classifier.FalsingCollector
24 import com.android.systemui.dagger.SysUISingleton
25 import com.android.systemui.dump.DumpManager
26 import com.android.systemui.keyguard.WakefulnessLifecycle
27 import com.android.systemui.media.MediaHierarchyManager
28 import com.android.systemui.plugins.ActivityStarter.OnDismissAction
29 import com.android.systemui.plugins.FalsingManager
30 import com.android.systemui.plugins.qs.QS
31 import com.android.systemui.plugins.statusbar.StatusBarStateController
32 import com.android.systemui.statusbar.notification.collection.NotificationEntry
33 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
34 import com.android.systemui.statusbar.notification.row.ExpandableView
35 import com.android.systemui.statusbar.notification.stack.AmbientState
36 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
37 import com.android.systemui.statusbar.phone.KeyguardBypassController
38 import com.android.systemui.statusbar.phone.LSShadeTransitionLogger
39 import com.android.systemui.statusbar.phone.NotificationPanelViewController
40 import com.android.systemui.statusbar.phone.ScrimController
41 import com.android.systemui.statusbar.phone.StatusBar
42 import com.android.systemui.statusbar.policy.ConfigurationController
43 import com.android.systemui.util.Utils
44 import java.io.FileDescriptor
45 import java.io.PrintWriter
46 import javax.inject.Inject
47 
48 private const val SPRING_BACK_ANIMATION_LENGTH_MS = 375L
49 private const val RUBBERBAND_FACTOR_STATIC = 0.15f
50 private const val RUBBERBAND_FACTOR_EXPANDABLE = 0.5f
51 
52 /**
53  * A class that controls the lockscreen to shade transition
54  */
55 @SysUISingleton
56 class LockscreenShadeTransitionController @Inject constructor(
57     private val statusBarStateController: SysuiStatusBarStateController,
58     private val logger: LSShadeTransitionLogger,
59     private val keyguardBypassController: KeyguardBypassController,
60     private val lockScreenUserManager: NotificationLockscreenUserManager,
61     private val falsingCollector: FalsingCollector,
62     private val ambientState: AmbientState,
63     private val mediaHierarchyManager: MediaHierarchyManager,
64     private val scrimController: ScrimController,
65     private val depthController: NotificationShadeDepthController,
66     private val context: Context,
67     wakefulnessLifecycle: WakefulnessLifecycle,
68     configurationController: ConfigurationController,
69     falsingManager: FalsingManager,
70     dumpManager: DumpManager,
71 ) : Dumpable {
72     private var pulseHeight: Float = 0f
73     private var useSplitShade: Boolean = false
74     private lateinit var nsslController: NotificationStackScrollLayoutController
75     lateinit var notificationPanelController: NotificationPanelViewController
76     lateinit var statusbar: StatusBar
77     lateinit var qS: QS
78 
79     /**
80      * A handler that handles the next keyguard dismiss animation.
81      */
82     private var animationHandlerOnKeyguardDismiss: ((Long) -> Unit)? = null
83 
84     /**
85      * The entry that was just dragged down on.
86      */
87     private var draggedDownEntry: NotificationEntry? = null
88 
89     /**
90      * The current animator if any
91      */
92     @VisibleForTesting
93     internal var dragDownAnimator: ValueAnimator? = null
94 
95     /**
96      * The current pulse height animator if any
97      */
98     @VisibleForTesting
99     internal var pulseHeightAnimator: ValueAnimator? = null
100 
101     /**
102      * Distance that the full shade transition takes in order for scrim to fully transition to
103      * the shade (in alpha)
104      */
105     private var scrimTransitionDistance = 0
106 
107     /**
108      * Distance that the full transition takes in order for us to fully transition to the shade
109      */
110     private var fullTransitionDistance = 0
111 
112     /**
113      * Flag to make sure that the dragDownAmount is applied to the listeners even when in the
114      * locked down shade.
115      */
116     private var forceApplyAmount = false
117 
118     /**
119      * A flag to suppress the default animation when unlocking in the locked down shade.
120      */
121     private var nextHideKeyguardNeedsNoAnimation = false
122 
123     /**
124      * Are we currently waking up to the shade locked
125      */
126     var isWakingToShadeLocked: Boolean = false
127         private set
128 
129     /**
130      * The distance until we're showing the notifications when pulsing
131      */
132     val distanceUntilShowingPulsingNotifications
133         get() = scrimTransitionDistance
134 
135     /**
136      * The udfpsKeyguardViewController if it exists.
137      */
138     var udfpsKeyguardViewController: UdfpsKeyguardViewController? = null
139 
140     /**
141      * The touch helper responsible for the drag down animation.
142      */
143     val touchHelper = DragDownHelper(falsingManager, falsingCollector, this, context)
144 
145     init {
146         updateResources()
147         configurationController.addCallback(object : ConfigurationController.ConfigurationListener {
148             override fun onConfigChanged(newConfig: Configuration?) {
149                 updateResources()
150                 touchHelper.updateResources(context)
151             }
152         })
153         dumpManager.registerDumpable(this)
154         statusBarStateController.addCallback(object : StatusBarStateController.StateListener {
155             override fun onExpandedChanged(isExpanded: Boolean) {
156                 // safeguard: When the panel is fully collapsed, let's make sure to reset.
157                 // See b/198098523
158                 if (!isExpanded) {
159                     if (dragDownAmount != 0f && dragDownAnimator?.isRunning != true) {
160                         logger.logDragDownAmountResetWhenFullyCollapsed()
161                         dragDownAmount = 0f
162                     }
163                     if (pulseHeight != 0f && pulseHeightAnimator?.isRunning != true) {
164                         logger.logPulseHeightNotResetWhenFullyCollapsed()
165                         setPulseHeight(0f, animate = false)
166                     }
167                 }
168             }
169         })
170         wakefulnessLifecycle.addObserver(object : WakefulnessLifecycle.Observer {
171             override fun onPostFinishedWakingUp() {
172                 // when finishing waking up, the UnlockedScreenOffAnimation has another attempt
173                 // to reset keyguard. Let's do it in post
174                 isWakingToShadeLocked = false
175             }
176         })
177     }
178 
179     private fun updateResources() {
180         scrimTransitionDistance = context.resources.getDimensionPixelSize(
181                 R.dimen.lockscreen_shade_scrim_transition_distance)
182         fullTransitionDistance = context.resources.getDimensionPixelSize(
183                 R.dimen.lockscreen_shade_qs_transition_distance)
184         useSplitShade = Utils.shouldUseSplitNotificationShade(context.resources)
185     }
186 
187     fun setStackScroller(nsslController: NotificationStackScrollLayoutController) {
188         this.nsslController = nsslController
189         touchHelper.host = nsslController.view
190         touchHelper.expandCallback = nsslController.expandHelperCallback
191     }
192 
193     /**
194      * Initialize the shelf controller such that clicks on it will expand the shade
195      */
196     fun bindController(notificationShelfController: NotificationShelfController) {
197         // Bind the click listener of the shelf to go to the full shade
198         notificationShelfController.setOnClickListener {
199             if (statusBarStateController.state == StatusBarState.KEYGUARD) {
200                 statusbar.wakeUpIfDozing(SystemClock.uptimeMillis(), it, "SHADE_CLICK")
201                 goToLockedShade(it)
202             }
203         }
204     }
205 
206     /**
207      * @return true if the interaction is accepted, false if it should be cancelled
208      */
209     internal fun canDragDown(): Boolean {
210         return (statusBarStateController.state == StatusBarState.KEYGUARD ||
211                 nsslController.isInLockedDownShade()) &&
212                 (qS.isFullyCollapsed || useSplitShade)
213     }
214 
215     /**
216      * Called by the touch helper when when a gesture has completed all the way and released.
217      */
218     internal fun onDraggedDown(startingChild: View?, dragLengthY: Int) {
219         if (canDragDown()) {
220             val cancelRunnable = Runnable {
221                 logger.logGoingToLockedShadeAborted()
222                 setDragDownAmountAnimated(0f)
223             }
224             if (nsslController.isInLockedDownShade()) {
225                 logger.logDraggedDownLockDownShade(startingChild)
226                 statusBarStateController.setLeaveOpenOnKeyguardHide(true)
227                 statusbar.dismissKeyguardThenExecute(OnDismissAction {
228                     nextHideKeyguardNeedsNoAnimation = true
229                     false
230                 }, cancelRunnable, false /* afterKeyguardGone */)
231             } else {
232                 logger.logDraggedDown(startingChild, dragLengthY)
233                 if (!ambientState.isDozing() || startingChild != null) {
234                     // go to locked shade while animating the drag down amount from its current
235                     // value
236                     val animationHandler = { delay: Long ->
237                         if (startingChild is ExpandableNotificationRow) {
238                             startingChild.onExpandedByGesture(
239                                     true /* drag down is always an open */)
240                         }
241                         notificationPanelController.animateToFullShade(delay)
242                         notificationPanelController.setTransitionToFullShadeAmount(0f,
243                                 true /* animated */, delay)
244 
245                         // Let's reset ourselves, ready for the next animation
246 
247                         // changing to shade locked will make isInLockDownShade true, so let's
248                         // override that
249                         forceApplyAmount = true
250                         // Reset the behavior. At this point the animation is already started
251                         dragDownAmount = 0f
252                         forceApplyAmount = false
253                     }
254                     goToLockedShadeInternal(startingChild, animationHandler, cancelRunnable)
255                 }
256             }
257         } else {
258             logger.logUnSuccessfulDragDown(startingChild)
259             setDragDownAmountAnimated(0f)
260         }
261     }
262 
263     /**
264      * Called by the touch helper when the drag down was aborted and should be reset.
265      */
266     internal fun onDragDownReset() {
267         logger.logDragDownAborted()
268         nsslController.setDimmed(true /* dimmed */, true /* animated */)
269         nsslController.resetScrollPosition()
270         nsslController.resetCheckSnoozeLeavebehind()
271         setDragDownAmountAnimated(0f)
272     }
273 
274     /**
275      * The user has dragged either above or below the threshold which changes the dimmed state.
276      * @param above whether they dragged above it
277      */
278     internal fun onCrossedThreshold(above: Boolean) {
279         nsslController.setDimmed(!above /* dimmed */, true /* animate */)
280     }
281 
282     /**
283      * Called by the touch helper when the drag down was started
284      */
285     internal fun onDragDownStarted(startingChild: ExpandableView?) {
286         logger.logDragDownStarted(startingChild)
287         nsslController.cancelLongPress()
288         nsslController.checkSnoozeLeavebehind()
289         dragDownAnimator?.apply {
290             if (isRunning) {
291                 logger.logAnimationCancelled(isPulse = false)
292                 cancel()
293             }
294         }
295     }
296 
297     /**
298      * Do we need a falsing check currently?
299      */
300     internal val isFalsingCheckNeeded: Boolean
301         get() = statusBarStateController.state == StatusBarState.KEYGUARD
302 
303     /**
304      * Is dragging down enabled on a given view
305      * @param view The view to check or `null` to check if it's enabled at all
306      */
307     internal fun isDragDownEnabledForView(view: ExpandableView?): Boolean {
308         if (isDragDownAnywhereEnabled) {
309             return true
310         }
311         if (nsslController.isInLockedDownShade()) {
312             if (view == null) {
313                 // Dragging down is allowed in general
314                 return true
315             }
316             if (view is ExpandableNotificationRow) {
317                 // Only drag down on sensitive views, otherwise the ExpandHelper will take this
318                 return view.entry.isSensitive
319             }
320         }
321         return false
322     }
323 
324     /**
325      * @return if drag down is enabled anywhere, not just on selected views.
326      */
327     internal val isDragDownAnywhereEnabled: Boolean
328         get() = (statusBarStateController.getState() == StatusBarState.KEYGUARD &&
329                 !keyguardBypassController.bypassEnabled &&
330                 (qS.isFullyCollapsed || useSplitShade))
331 
332     /**
333      * The amount in pixels that the user has dragged down.
334      */
335     internal var dragDownAmount = 0f
336         set(value) {
337             if (field != value || forceApplyAmount) {
338                 field = value
339                 if (!nsslController.isInLockedDownShade() || field == 0f || forceApplyAmount) {
340                     nsslController.setTransitionToFullShadeAmount(field)
341                     notificationPanelController.setTransitionToFullShadeAmount(field,
342                             false /* animate */, 0 /* delay */)
343                     val progress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
344                     qS.setTransitionToFullShadeAmount(field, progress)
345                     // TODO: appear media also in split shade
346                     val mediaAmount = if (useSplitShade) 0f else field
347                     mediaHierarchyManager.setTransitionToFullShadeAmount(mediaAmount)
348                     transitionToShadeAmountCommon(field)
349                 }
350             }
351         }
352 
353     private fun transitionToShadeAmountCommon(dragDownAmount: Float) {
354         val scrimProgress = MathUtils.saturate(dragDownAmount / scrimTransitionDistance)
355         scrimController.setTransitionToFullShadeProgress(scrimProgress)
356         // Fade out all content only visible on the lockscreen
357         notificationPanelController.setKeyguardOnlyContentAlpha(1.0f - scrimProgress)
358         depthController.transitionToFullShadeProgress = scrimProgress
359         udfpsKeyguardViewController?.setTransitionToFullShadeProgress(scrimProgress)
360     }
361 
362     private fun setDragDownAmountAnimated(
363         target: Float,
364         delay: Long = 0,
365         endlistener: (() -> Unit)? = null
366     ) {
367         logger.logDragDownAnimation(target)
368         val dragDownAnimator = ValueAnimator.ofFloat(dragDownAmount, target)
369         dragDownAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN
370         dragDownAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS
371         dragDownAnimator.addUpdateListener { animation: ValueAnimator ->
372             dragDownAmount = animation.animatedValue as Float
373         }
374         if (delay > 0) {
375             dragDownAnimator.startDelay = delay
376         }
377         if (endlistener != null) {
378             dragDownAnimator.addListener(object : AnimatorListenerAdapter() {
379                 override fun onAnimationEnd(animation: Animator?) {
380                     endlistener.invoke()
381                 }
382             })
383         }
384         dragDownAnimator.start()
385         this.dragDownAnimator = dragDownAnimator
386     }
387 
388     /**
389      * Animate appear the drag down amount.
390      */
391     private fun animateAppear(delay: Long = 0) {
392         // changing to shade locked will make isInLockDownShade true, so let's override
393         // that
394         forceApplyAmount = true
395 
396         // we set the value initially to 1 pixel, since that will make sure we're
397         // transitioning to the full shade. this is important to avoid flickering,
398         // as the below animation only starts once the shade is unlocked, which can
399         // be a couple of frames later. if we're setting it to 0, it will use the
400         // default inset and therefore flicker
401         dragDownAmount = 1f
402         setDragDownAmountAnimated(fullTransitionDistance.toFloat(), delay = delay) {
403             // End listener:
404             // Reset
405             dragDownAmount = 0f
406             forceApplyAmount = false
407         }
408     }
409 
410     /**
411      * Ask this controller to go to the locked shade, changing the state change and doing
412      * an animation, where the qs appears from 0 from the top
413      *
414      * If secure with redaction: Show bouncer, go to unlocked shade.
415      * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
416      *
417      * @param expandView The view to expand after going to the shade
418      * @param needsQSAnimation if this needs the quick settings to slide in from the top or if
419      *                         that's already handled separately
420      */
421     @JvmOverloads
422     fun goToLockedShade(expandedView: View?, needsQSAnimation: Boolean = true) {
423         val isKeyguard = statusBarStateController.state == StatusBarState.KEYGUARD
424         logger.logTryGoToLockedShade(isKeyguard)
425         if (isKeyguard) {
426             val animationHandler: ((Long) -> Unit)?
427             if (needsQSAnimation) {
428                 // Let's use the default animation
429                 animationHandler = null
430             } else {
431                 // Let's only animate notifications
432                 animationHandler = { delay: Long ->
433                     notificationPanelController.animateToFullShade(delay)
434                 }
435             }
436             goToLockedShadeInternal(expandedView, animationHandler,
437                     cancelAction = null)
438         }
439     }
440 
441     /**
442      * If secure with redaction: Show bouncer, go to unlocked shade.
443      *
444      * If secure without redaction or no security: Go to [StatusBarState.SHADE_LOCKED].
445      *
446      * @param expandView The view to expand after going to the shade.
447      * @param animationHandler The handler which performs the go to full shade animation. If null,
448      *                         the default handler will do the animation, otherwise the caller is
449      *                         responsible for the animation. The input value is a Long for the
450      *                         delay for the animation.
451      * @param cancelAction The runnable to invoke when the transition is aborted. This happens if
452      *                     the user goes to the bouncer and goes back.
453      */
454     private fun goToLockedShadeInternal(
455         expandView: View?,
456         animationHandler: ((Long) -> Unit)? = null,
457         cancelAction: Runnable? = null
458     ) {
459         if (statusbar.isShadeDisabled) {
460             cancelAction?.run()
461             logger.logShadeDisabledOnGoToLockedShade()
462             return
463         }
464         var userId: Int = lockScreenUserManager.getCurrentUserId()
465         var entry: NotificationEntry? = null
466         if (expandView is ExpandableNotificationRow) {
467             entry = expandView.entry
468             entry.setUserExpanded(true /* userExpanded */, true /* allowChildExpansion */)
469             // Indicate that the group expansion is changing at this time -- this way the group
470             // and children backgrounds / divider animations will look correct.
471             entry.setGroupExpansionChanging(true)
472             userId = entry.sbn.userId
473         }
474         var fullShadeNeedsBouncer = (!lockScreenUserManager.userAllowsPrivateNotificationsInPublic(
475                 lockScreenUserManager.getCurrentUserId()) ||
476                 !lockScreenUserManager.shouldShowLockscreenNotifications() ||
477                 falsingCollector.shouldEnforceBouncer())
478         if (keyguardBypassController.bypassEnabled) {
479             fullShadeNeedsBouncer = false
480         }
481         if (lockScreenUserManager.isLockscreenPublicMode(userId) && fullShadeNeedsBouncer) {
482             statusBarStateController.setLeaveOpenOnKeyguardHide(true)
483             var onDismissAction: OnDismissAction? = null
484             if (animationHandler != null) {
485                 onDismissAction = OnDismissAction {
486                     // We're waiting on keyguard to hide before triggering the action,
487                     // as that will make the animation work properly
488                     animationHandlerOnKeyguardDismiss = animationHandler
489                     false
490                 }
491             }
492             val cancelHandler = Runnable {
493                 draggedDownEntry?.apply {
494                     setUserLocked(false)
495                     notifyHeightChanged(false /* needsAnimation */)
496                     draggedDownEntry = null
497                 }
498                 cancelAction?.run()
499             }
500             logger.logShowBouncerOnGoToLockedShade()
501             statusbar.showBouncerWithDimissAndCancelIfKeyguard(onDismissAction, cancelHandler)
502             draggedDownEntry = entry
503         } else {
504             logger.logGoingToLockedShade(animationHandler != null)
505             if (statusBarStateController.isDozing) {
506                 // Make sure we don't go back to keyguard immediately again after waking up
507                 isWakingToShadeLocked = true
508             }
509             statusBarStateController.setState(StatusBarState.SHADE_LOCKED)
510             // This call needs to be after updating the shade state since otherwise
511             // the scrimstate resets too early
512             if (animationHandler != null) {
513                 animationHandler.invoke(0 /* delay */)
514             } else {
515                 performDefaultGoToFullShadeAnimation(0)
516             }
517         }
518     }
519 
520     /**
521      * Notify this handler that the keyguard was just dismissed and that a animation to
522      * the full shade should happen.
523      *
524      * @param delay the delay to do the animation with
525      * @param previousState which state were we in when we hid the keyguard?
526      */
527     fun onHideKeyguard(delay: Long, previousState: Int) {
528         logger.logOnHideKeyguard()
529         if (animationHandlerOnKeyguardDismiss != null) {
530             animationHandlerOnKeyguardDismiss!!.invoke(delay)
531             animationHandlerOnKeyguardDismiss = null
532         } else {
533             if (nextHideKeyguardNeedsNoAnimation) {
534                 nextHideKeyguardNeedsNoAnimation = false
535             } else if (previousState != StatusBarState.SHADE_LOCKED) {
536                 // No animation necessary if we already were in the shade locked!
537                 performDefaultGoToFullShadeAnimation(delay)
538             }
539         }
540         draggedDownEntry?.apply {
541             setUserLocked(false)
542             draggedDownEntry = null
543         }
544     }
545 
546     /**
547      * Perform the default appear animation when going to the full shade. This is called when
548      * not triggered by gestures, e.g. when clicking on the shelf or expand button.
549      */
550     private fun performDefaultGoToFullShadeAnimation(delay: Long) {
551         logger.logDefaultGoToFullShadeAnimation(delay)
552         notificationPanelController.animateToFullShade(delay)
553         animateAppear(delay)
554     }
555 
556     //
557     // PULSE EXPANSION
558     //
559 
560     /**
561      * Set the height how tall notifications are pulsing. This is only set whenever we are expanding
562      * from a pulse and determines how much the notifications are expanded.
563      */
564     fun setPulseHeight(height: Float, animate: Boolean = false) {
565         if (animate) {
566             val pulseHeightAnimator = ValueAnimator.ofFloat(pulseHeight, height)
567             pulseHeightAnimator.interpolator = Interpolators.FAST_OUT_SLOW_IN
568             pulseHeightAnimator.duration = SPRING_BACK_ANIMATION_LENGTH_MS
569             pulseHeightAnimator.addUpdateListener { animation: ValueAnimator ->
570                 setPulseHeight(animation.animatedValue as Float)
571             }
572             pulseHeightAnimator.start()
573             this.pulseHeightAnimator = pulseHeightAnimator
574         } else {
575             pulseHeight = height
576             val overflow = nsslController.setPulseHeight(height)
577             notificationPanelController.setOverStrechAmount(overflow)
578             val transitionHeight = if (keyguardBypassController.bypassEnabled) height else 0.0f
579             transitionToShadeAmountCommon(transitionHeight)
580         }
581     }
582 
583     /**
584      * Finish the pulse animation when the touch interaction finishes
585      * @param cancelled was the interaction cancelled and this is a reset?
586      */
587     fun finishPulseAnimation(cancelled: Boolean) {
588         logger.logPulseExpansionFinished(cancelled)
589         if (cancelled) {
590             setPulseHeight(0f, animate = true)
591         } else {
592             notificationPanelController.onPulseExpansionFinished()
593             setPulseHeight(0f, animate = false)
594         }
595     }
596 
597     /**
598      * Notify this class that a pulse expansion is starting
599      */
600     fun onPulseExpansionStarted() {
601         logger.logPulseExpansionStarted()
602         pulseHeightAnimator?.apply {
603             if (isRunning) {
604                 logger.logAnimationCancelled(isPulse = true)
605                 cancel()
606             }
607         }
608     }
609 
610     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
611         IndentingPrintWriter(pw, "  ").let {
612             it.println("LSShadeTransitionController:")
613             it.increaseIndent()
614             it.println("pulseHeight: $pulseHeight")
615             it.println("useSplitShade: $useSplitShade")
616             it.println("dragDownAmount: $dragDownAmount")
617             it.println("isDragDownAnywhereEnabled: $isDragDownAnywhereEnabled")
618             it.println("isFalsingCheckNeeded: $isFalsingCheckNeeded")
619             it.println("isWakingToShadeLocked: $isWakingToShadeLocked")
620             it.println("hasPendingHandlerOnKeyguardDismiss: " +
621                 "${animationHandlerOnKeyguardDismiss != null}")
622         }
623     }
624 }
625 
626 /**
627  * A utility class to enable the downward swipe on the lockscreen to go to the full shade and expand
628  * the notification where the drag started.
629  */
630 class DragDownHelper(
631     private val falsingManager: FalsingManager,
632     private val falsingCollector: FalsingCollector,
633     private val dragDownCallback: LockscreenShadeTransitionController,
634     context: Context
635 ) : Gefingerpoken {
636 
637     private var dragDownAmountOnStart = 0.0f
638     lateinit var expandCallback: ExpandHelper.Callback
639     lateinit var host: View
640 
641     private var minDragDistance = 0
642     private var initialTouchX = 0f
643     private var initialTouchY = 0f
644     private var touchSlop = 0f
645     private var slopMultiplier = 0f
646     private val temp2 = IntArray(2)
647     private var draggedFarEnough = false
648     private var startingChild: ExpandableView? = null
649     private var lastHeight = 0f
650     var isDraggingDown = false
651         private set
652 
653     private val isFalseTouch: Boolean
654         get() {
655             return if (!dragDownCallback.isFalsingCheckNeeded) {
656                 false
657             } else {
658                 falsingManager.isFalseTouch(Classifier.NOTIFICATION_DRAG_DOWN) || !draggedFarEnough
659             }
660         }
661 
662     val isDragDownEnabled: Boolean
663         get() = dragDownCallback.isDragDownEnabledForView(null)
664 
665     init {
666         updateResources(context)
667     }
668 
669     fun updateResources(context: Context) {
670         minDragDistance = context.resources.getDimensionPixelSize(
671                 R.dimen.keyguard_drag_down_min_distance)
672         val configuration = ViewConfiguration.get(context)
673         touchSlop = configuration.scaledTouchSlop.toFloat()
674         slopMultiplier = configuration.scaledAmbiguousGestureMultiplier
675     }
676 
677     override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
678         val x = event.x
679         val y = event.y
680         when (event.actionMasked) {
681             MotionEvent.ACTION_DOWN -> {
682                 draggedFarEnough = false
683                 isDraggingDown = false
684                 startingChild = null
685                 initialTouchY = y
686                 initialTouchX = x
687             }
688             MotionEvent.ACTION_MOVE -> {
689                 val h = y - initialTouchY
690                 // Adjust the touch slop if another gesture may be being performed.
691                 val touchSlop = if (event.classification
692                         == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE)
693                     touchSlop * slopMultiplier
694                 else
695                     touchSlop
696                 if (h > touchSlop && h > Math.abs(x - initialTouchX)) {
697                     falsingCollector.onNotificationStartDraggingDown()
698                     isDraggingDown = true
699                     captureStartingChild(initialTouchX, initialTouchY)
700                     initialTouchY = y
701                     initialTouchX = x
702                     dragDownCallback.onDragDownStarted(startingChild)
703                     dragDownAmountOnStart = dragDownCallback.dragDownAmount
704                     return startingChild != null || dragDownCallback.isDragDownAnywhereEnabled
705                 }
706             }
707         }
708         return false
709     }
710 
711     override fun onTouchEvent(event: MotionEvent): Boolean {
712         if (!isDraggingDown) {
713             return false
714         }
715         val x = event.x
716         val y = event.y
717         when (event.actionMasked) {
718             MotionEvent.ACTION_MOVE -> {
719                 lastHeight = y - initialTouchY
720                 captureStartingChild(initialTouchX, initialTouchY)
721                 dragDownCallback.dragDownAmount = lastHeight + dragDownAmountOnStart
722                 if (startingChild != null) {
723                     handleExpansion(lastHeight, startingChild!!)
724                 }
725                 if (lastHeight > minDragDistance) {
726                     if (!draggedFarEnough) {
727                         draggedFarEnough = true
728                         dragDownCallback.onCrossedThreshold(true)
729                     }
730                 } else {
731                     if (draggedFarEnough) {
732                         draggedFarEnough = false
733                         dragDownCallback.onCrossedThreshold(false)
734                     }
735                 }
736                 return true
737             }
738             MotionEvent.ACTION_UP -> if (!falsingManager.isUnlockingDisabled && !isFalseTouch &&
739                     dragDownCallback.canDragDown()) {
740                 dragDownCallback.onDraggedDown(startingChild, (y - initialTouchY).toInt())
741                 if (startingChild != null) {
742                     expandCallback.setUserLockedChild(startingChild, false)
743                     startingChild = null
744                 }
745                 isDraggingDown = false
746             } else {
747                 stopDragging()
748                 return false
749             }
750             MotionEvent.ACTION_CANCEL -> {
751                 stopDragging()
752                 return false
753             }
754         }
755         return false
756     }
757 
758     private fun captureStartingChild(x: Float, y: Float) {
759         if (startingChild == null) {
760             startingChild = findView(x, y)
761             if (startingChild != null) {
762                 if (dragDownCallback.isDragDownEnabledForView(startingChild)) {
763                     expandCallback.setUserLockedChild(startingChild, true)
764                 } else {
765                     startingChild = null
766                 }
767             }
768         }
769     }
770 
771     private fun handleExpansion(heightDelta: Float, child: ExpandableView) {
772         var hDelta = heightDelta
773         if (hDelta < 0) {
774             hDelta = 0f
775         }
776         val expandable = child.isContentExpandable
777         val rubberbandFactor = if (expandable) {
778             RUBBERBAND_FACTOR_EXPANDABLE
779         } else {
780             RUBBERBAND_FACTOR_STATIC
781         }
782         var rubberband = hDelta * rubberbandFactor
783         if (expandable && rubberband + child.collapsedHeight > child.maxContentHeight) {
784             var overshoot = rubberband + child.collapsedHeight - child.maxContentHeight
785             overshoot *= 1 - RUBBERBAND_FACTOR_STATIC
786             rubberband -= overshoot
787         }
788         child.actualHeight = (child.collapsedHeight + rubberband).toInt()
789     }
790 
791     private fun cancelChildExpansion(child: ExpandableView) {
792         if (child.actualHeight == child.collapsedHeight) {
793             expandCallback.setUserLockedChild(child, false)
794             return
795         }
796         val anim = ObjectAnimator.ofInt(child, "actualHeight",
797                 child.actualHeight, child.collapsedHeight)
798         anim.interpolator = Interpolators.FAST_OUT_SLOW_IN
799         anim.duration = SPRING_BACK_ANIMATION_LENGTH_MS
800         anim.addListener(object : AnimatorListenerAdapter() {
801             override fun onAnimationEnd(animation: Animator) {
802                 expandCallback.setUserLockedChild(child, false)
803             }
804         })
805         anim.start()
806     }
807 
808     private fun stopDragging() {
809         falsingCollector.onNotificationStopDraggingDown()
810         if (startingChild != null) {
811             cancelChildExpansion(startingChild!!)
812             startingChild = null
813         }
814         isDraggingDown = false
815         dragDownCallback.onDragDownReset()
816     }
817 
818     private fun findView(x: Float, y: Float): ExpandableView? {
819         host.getLocationOnScreen(temp2)
820         return expandCallback.getChildAtRawPosition(x + temp2[0], y + temp2[1])
821     }
822 }