1 /*
<lambda>null2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.navigationbar.gestural
17 
18 import android.content.Context
19 import android.content.res.Configuration
20 import android.graphics.Color
21 import android.graphics.Paint
22 import android.graphics.Point
23 import android.os.Handler
24 import android.util.Log
25 import android.util.MathUtils
26 import android.view.Gravity
27 import android.view.HapticFeedbackConstants
28 import android.view.MotionEvent
29 import android.view.VelocityTracker
30 import android.view.ViewConfiguration
31 import android.view.WindowManager
32 import androidx.annotation.VisibleForTesting
33 import androidx.core.os.postDelayed
34 import androidx.core.view.isVisible
35 import androidx.dynamicanimation.animation.DynamicAnimation
36 import com.android.internal.jank.Cuj
37 import com.android.internal.jank.InteractionJankMonitor
38 import com.android.internal.util.LatencyTracker
39 import com.android.systemui.plugins.NavigationEdgeBackPlugin
40 import com.android.systemui.statusbar.VibratorHelper
41 import com.android.systemui.statusbar.policy.ConfigurationController
42 import com.android.systemui.util.ViewController
43 import com.android.systemui.util.concurrency.BackPanelUiThread
44 import com.android.systemui.util.concurrency.UiThreadContext
45 import com.android.systemui.util.time.SystemClock
46 import java.io.PrintWriter
47 import javax.inject.Inject
48 import kotlin.math.abs
49 import kotlin.math.max
50 import kotlin.math.min
51 import kotlin.math.sign
52 
53 private const val TAG = "BackPanelController"
54 private const val ENABLE_FAILSAFE = true
55 private const val FAILSAFE_DELAY_MS = 350L
56 
57 private const val PX_PER_SEC = 1000
58 private const val PX_PER_MS = 1
59 
60 internal const val MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION = 300L
61 private const val MIN_DURATION_ACTIVE_AFTER_INACTIVE_ANIMATION = 130L
62 private const val MIN_DURATION_CANCELLED_ANIMATION = 200L
63 private const val MIN_DURATION_COMMITTED_ANIMATION = 80L
64 private const val MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION = 120L
65 private const val MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION = 50L
66 private const val MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION = 160F
67 private const val MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 10F
68 internal const val MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION = 100F
69 private const val MIN_DURATION_FLING_ANIMATION = 160L
70 
71 private const val MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING = 100L
72 private const val MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING = 400L
73 
74 private const val POP_ON_FLING_DELAY = 60L
75 private const val POP_ON_FLING_VELOCITY = 2f
76 private const val POP_ON_COMMITTED_VELOCITY = 3f
77 private const val POP_ON_ENTRY_TO_ACTIVE_VELOCITY = 4.5f
78 private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f
79 private const val POP_ON_INACTIVE_VELOCITY = -1.5f
80 
81 private const val DEBUG = false
82 
83 class BackPanelController
84 internal constructor(
85     context: Context,
86     private val windowManager: WindowManager,
87     private val viewConfiguration: ViewConfiguration,
88     private val mainHandler: Handler,
89     private val systemClock: SystemClock,
90     private val vibratorHelper: VibratorHelper,
91     private val configurationController: ConfigurationController,
92     latencyTracker: LatencyTracker,
93     private val interactionJankMonitor: InteractionJankMonitor,
94 ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
95 
96     /**
97      * Injectable instance to create a new BackPanelController.
98      *
99      * Necessary because EdgeBackGestureHandler sometimes needs to create new instances of
100      * BackPanelController, and we need to match EdgeBackGestureHandler's context.
101      */
102     class Factory
103     @Inject
104     constructor(
105         private val windowManager: WindowManager,
106         private val viewConfiguration: ViewConfiguration,
107         @BackPanelUiThread private val uiThreadContext: UiThreadContext,
108         private val systemClock: SystemClock,
109         private val vibratorHelper: VibratorHelper,
110         private val configurationController: ConfigurationController,
111         private val latencyTracker: LatencyTracker,
112         private val interactionJankMonitor: InteractionJankMonitor,
113     ) {
114         /** Construct a [BackPanelController]. */
115         fun create(context: Context): BackPanelController {
116             uiThreadContext.isCurrentThread()
117             return BackPanelController(
118                     context,
119                     windowManager,
120                     viewConfiguration,
121                     uiThreadContext.handler,
122                     systemClock,
123                     vibratorHelper,
124                     configurationController,
125                     latencyTracker,
126                     interactionJankMonitor
127                 )
128                 .also { it.init() }
129         }
130     }
131 
132     @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources)
133     @VisibleForTesting internal var currentState: GestureState = GestureState.GONE
134     private var previousState: GestureState = GestureState.GONE
135 
136     // Screen attributes
137     private lateinit var layoutParams: WindowManager.LayoutParams
138     private val displaySize = Point()
139 
140     private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
141     private var previousXTranslationOnActiveOffset = 0f
142     private var previousXTranslation = 0f
143     private var totalTouchDeltaActive = 0f
144     private var totalTouchDeltaInactive = 0f
145     private var touchDeltaStartX = 0f
146     private var velocityTracker: VelocityTracker? = null
147         set(value) {
148             if (field != value) field?.recycle()
149             field = value
150         }
151         get() {
152             if (field == null) field = VelocityTracker.obtain()
153             return field
154         }
155 
156     // The x,y position of the first touch event
157     private var startX = 0f
158     private var startY = 0f
159     private var startIsLeft: Boolean? = null
160 
161     private var gestureEntryTime = 0L
162     private var gestureInactiveTime = 0L
163 
164     private val elapsedTimeSinceInactive
165         get() = systemClock.uptimeMillis() - gestureInactiveTime
166 
167     private val elapsedTimeSinceEntry
168         get() = systemClock.uptimeMillis() - gestureEntryTime
169 
170     private var pastThresholdWhileEntryOrInactiveTime = 0L
171     private var entryToActiveDelay = 0F
172     private val entryToActiveDelayCalculation = {
173         convertVelocityToAnimationFactor(
174             valueOnFastVelocity = MIN_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION,
175             valueOnSlowVelocity = MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION,
176         )
177     }
178 
179     // Whether the current gesture has moved a sufficiently large amount,
180     // so that we can unambiguously start showing the ENTRY animation
181     private var hasPassedDragSlop = false
182 
183     // Distance in pixels a drag can be considered for a fling event
184     private var minFlingDistance = 0
185 
186     internal val failsafeRunnable = Runnable { onFailsafe() }
187 
188     internal enum class GestureState {
189         /* Arrow is off the screen and invisible */
190         GONE,
191 
192         /* Arrow is animating in */
193         ENTRY,
194 
195         /* releasing will commit back */
196         ACTIVE,
197 
198         /* releasing will cancel back */
199         INACTIVE,
200 
201         /* like committed, but animation takes longer */
202         FLUNG,
203 
204         /* back action currently occurring, arrow soon to be GONE */
205         COMMITTED,
206 
207         /* back action currently cancelling, arrow soon to be GONE */
208         CANCELLED
209     }
210 
211     /**
212      * Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The
213      * runnable is not called if the animation is cancelled
214      */
215     inner class DelayedOnAnimationEndListener
216     internal constructor(
217         private val handler: Handler,
218         private val runnableDelay: Long,
219         val runnable: Runnable,
220     ) : DynamicAnimation.OnAnimationEndListener {
221 
222         override fun onAnimationEnd(
223             animation: DynamicAnimation<*>,
224             canceled: Boolean,
225             value: Float,
226             velocity: Float
227         ) {
228             animation.removeEndListener(this)
229 
230             if (!canceled) {
231                 // The delay between finishing this animation and starting the runnable
232                 val delay = max(0, runnableDelay - elapsedTimeSinceEntry)
233 
234                 handler.postDelayed(runnable, delay)
235             }
236         }
237 
238         internal fun run() = runnable.run()
239     }
240 
241     private val onEndSetCommittedStateListener =
242         DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) }
243 
244     private val onEndSetGoneStateListener =
245         DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) {
246             cancelFailsafe()
247             updateArrowState(GestureState.GONE)
248         }
249 
250     private val onAlphaEndSetGoneStateListener =
251         DelayedOnAnimationEndListener(mainHandler, 0L) {
252             updateRestingArrowDimens()
253             if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) {
254                 scheduleFailsafe()
255             }
256         }
257 
258     // Minimum of the screen's width or the predefined threshold
259     private var fullyStretchedThreshold = 0f
260 
261     /** Used for initialization and configuration changes */
262     private fun updateConfiguration() {
263         params.update(resources)
264         mView.updateArrowPaint(params.arrowThickness)
265         minFlingDistance = viewConfiguration.scaledTouchSlop * 3
266     }
267 
268     private val configurationListener =
269         object : ConfigurationController.ConfigurationListener {
270             override fun onConfigChanged(newConfig: Configuration?) {
271                 updateConfiguration()
272             }
273 
274             override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) {
275                 updateArrowDirection(isLayoutRtl)
276             }
277         }
278 
279     override fun onViewAttached() {
280         updateConfiguration()
281         updateArrowDirection(configurationController.isLayoutRtl)
282         updateArrowState(GestureState.GONE, force = true)
283         updateRestingArrowDimens()
284         configurationController.addCallback(configurationListener)
285     }
286 
287     /** Update the arrow direction. The arrow should point the same way for both panels. */
288     private fun updateArrowDirection(isLayoutRtl: Boolean) {
289         mView.arrowsPointLeft = isLayoutRtl
290     }
291 
292     override fun onViewDetached() {
293         configurationController.removeCallback(configurationListener)
294     }
295 
296     override fun onMotionEvent(event: MotionEvent) {
297         velocityTracker!!.addMovement(event)
298         when (event.actionMasked) {
299             MotionEvent.ACTION_DOWN -> {
300                 cancelAllPendingAnimations()
301                 startX = event.x
302                 startY = event.y
303 
304                 updateArrowState(GestureState.GONE)
305                 updateYStartPosition(startY)
306 
307                 // reset animation properties
308                 startIsLeft = mView.isLeftPanel
309                 hasPassedDragSlop = false
310                 mView.resetStretch()
311             }
312             MotionEvent.ACTION_MOVE -> {
313                 if (dragSlopExceeded(event.x, startX)) {
314                     handleMoveEvent(event)
315                 }
316             }
317             MotionEvent.ACTION_UP -> {
318                 when (currentState) {
319                     GestureState.ENTRY -> {
320                         if (
321                             isFlungAwayFromEdge(endX = event.x) ||
322                                 previousXTranslation > params.staticTriggerThreshold
323                         ) {
324                             updateArrowState(GestureState.FLUNG)
325                         } else {
326                             updateArrowState(GestureState.CANCELLED)
327                         }
328                     }
329                     GestureState.INACTIVE -> {
330                         if (isFlungAwayFromEdge(endX = event.x)) {
331                             // This is called outside of updateArrowState so that
332                             // BackAnimationController can immediately evaluate state
333                             // instead of after the flung delay
334                             backCallback.setTriggerBack(true)
335                             mainHandler.postDelayed(MIN_DURATION_INACTIVE_BEFORE_FLUNG_ANIMATION) {
336                                 updateArrowState(GestureState.FLUNG)
337                             }
338                         } else {
339                             updateArrowState(GestureState.CANCELLED)
340                         }
341                     }
342                     GestureState.ACTIVE -> {
343                         if (
344                             previousState == GestureState.ENTRY &&
345                                 elapsedTimeSinceEntry <
346                                     MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING
347                         ) {
348                             updateArrowState(GestureState.FLUNG)
349                         } else if (
350                             previousState == GestureState.INACTIVE &&
351                                 elapsedTimeSinceInactive <
352                                     MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING
353                         ) {
354                             // A delay is added to allow the background to transition back to ACTIVE
355                             // since it was briefly in INACTIVE. Without this delay, setting it
356                             // immediately to COMMITTED would result in the committed animation
357                             // appearing like it was playing in INACTIVE.
358                             mainHandler.postDelayed(MIN_DURATION_ACTIVE_AFTER_INACTIVE_ANIMATION) {
359                                 updateArrowState(GestureState.COMMITTED)
360                             }
361                         } else {
362                             updateArrowState(GestureState.COMMITTED)
363                         }
364                     }
365                     GestureState.GONE,
366                     GestureState.FLUNG,
367                     GestureState.COMMITTED,
368                     GestureState.CANCELLED -> {
369                         updateArrowState(GestureState.CANCELLED)
370                     }
371                 }
372                 velocityTracker = null
373             }
374             MotionEvent.ACTION_CANCEL -> {
375                 // Receiving a CANCEL implies that something else intercepted
376                 // the gesture, i.e., the user did not cancel their gesture.
377                 // Therefore, disappear immediately, with minimum fanfare.
378                 interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW)
379                 updateArrowState(GestureState.GONE)
380                 velocityTracker = null
381             }
382         }
383     }
384 
385     private fun cancelAllPendingAnimations() {
386         cancelFailsafe()
387         mView.cancelAnimations()
388         mainHandler.removeCallbacks(onEndSetCommittedStateListener.runnable)
389         mainHandler.removeCallbacks(onEndSetGoneStateListener.runnable)
390         mainHandler.removeCallbacks(onAlphaEndSetGoneStateListener.runnable)
391     }
392 
393     /**
394      * Returns false until the current gesture exceeds the touch slop threshold, and returns true
395      * thereafter (we reset on the subsequent back gesture). The moment it switches from false ->
396      * true is important, because that's when we switch state, from GONE -> ENTRY.
397      *
398      * @return whether the current gesture has moved past a minimum threshold.
399      */
400     private fun dragSlopExceeded(curX: Float, startX: Float): Boolean {
401         if (hasPassedDragSlop) return true
402 
403         if (abs(curX - startX) > viewConfiguration.scaledEdgeSlop) {
404             // Reset the arrow to the side
405             updateArrowState(GestureState.ENTRY)
406 
407             windowManager.updateViewLayout(mView, layoutParams)
408             mView.startTrackingShowBackArrowLatency()
409 
410             hasPassedDragSlop = true
411         }
412         return hasPassedDragSlop
413     }
414 
415     private fun updateArrowStateOnMove(yTranslation: Float, xTranslation: Float) {
416         val isWithinYActivationThreshold = xTranslation * 2 >= yTranslation
417         val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold
418         when (currentState) {
419             GestureState.ENTRY -> {
420                 if (
421                     isPastThresholdToActive(
422                         isPastThreshold = isPastStaticThreshold,
423                         dynamicDelay = entryToActiveDelayCalculation
424                     )
425                 ) {
426                     updateArrowState(GestureState.ACTIVE)
427                 }
428             }
429             GestureState.INACTIVE -> {
430                 val isPastDynamicReactivationThreshold =
431                     totalTouchDeltaInactive >= params.reactivationTriggerThreshold
432 
433                 if (
434                     isPastThresholdToActive(
435                         isPastThreshold =
436                             isPastStaticThreshold &&
437                                 isPastDynamicReactivationThreshold &&
438                                 isWithinYActivationThreshold,
439                         delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION
440                     )
441                 ) {
442                     updateArrowState(GestureState.ACTIVE)
443                 }
444             }
445             GestureState.ACTIVE -> {
446                 val isPastDynamicDeactivationThreshold =
447                     totalTouchDeltaActive <= params.deactivationTriggerThreshold
448                 val isMinDurationElapsed =
449                     elapsedTimeSinceEntry > MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION
450                 val isPastAllThresholds =
451                     !isWithinYActivationThreshold || isPastDynamicDeactivationThreshold
452                 if (isPastAllThresholds && isMinDurationElapsed) {
453                     updateArrowState(GestureState.INACTIVE)
454                 }
455             }
456             else -> {}
457         }
458     }
459 
460     private fun handleMoveEvent(event: MotionEvent) {
461         val x = event.x
462         val y = event.y
463 
464         val yOffset = y - startY
465 
466         // How far in the y direction we are from the original touch
467         val yTranslation = abs(yOffset)
468 
469         // How far in the x direction we are from the original touch ignoring motion that
470         // occurs between the screen edge and the touch start.
471         val xTranslation = max(0f, if (mView.isLeftPanel) x - startX else startX - x)
472 
473         // Compared to last time, how far we moved in the x direction. If <0, we are moving closer
474         // to the edge. If >0, we are moving further from the edge
475         val xDelta = xTranslation - previousXTranslation
476         previousXTranslation = xTranslation
477 
478         if (abs(xDelta) > 0) {
479             val isInSameDirection = sign(xDelta) == sign(totalTouchDeltaActive)
480             val isInDynamicRange = totalTouchDeltaActive in params.dynamicTriggerThresholdRange
481             val isTouchInContinuousDirection = isInSameDirection || isInDynamicRange
482 
483             if (isTouchInContinuousDirection) {
484                 // Direction has NOT changed, so keep counting the delta
485                 totalTouchDeltaActive += xDelta
486             } else {
487                 // Direction has changed, so reset the delta
488                 totalTouchDeltaActive = xDelta
489                 touchDeltaStartX = x
490             }
491 
492             // Add a slop to to prevent small jitters when arrow is at edge in
493             // emitting small values that cause the arrow to poke out slightly
494             val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat()
495             totalTouchDeltaInactive =
496                 totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta)
497         }
498 
499         updateArrowStateOnMove(yTranslation, xTranslation)
500 
501         val gestureProgress =
502             when (currentState) {
503                 GestureState.ACTIVE -> fullScreenProgress(xTranslation)
504                 GestureState.ENTRY -> staticThresholdProgress(xTranslation)
505                 GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive)
506                 else -> null
507             }
508 
509         gestureProgress?.let {
510             when (currentState) {
511                 GestureState.ACTIVE -> stretchActiveBackIndicator(gestureProgress)
512                 GestureState.ENTRY -> stretchEntryBackIndicator(gestureProgress)
513                 GestureState.INACTIVE -> stretchInactiveBackIndicator(gestureProgress)
514                 else -> {}
515             }
516         }
517 
518         setArrowStrokeAlpha(gestureProgress)
519         setVerticalTranslation(yOffset)
520     }
521 
522     private fun setArrowStrokeAlpha(gestureProgress: Float?) {
523         val strokeAlphaProgress =
524             when (currentState) {
525                 GestureState.ENTRY -> gestureProgress
526                 GestureState.INACTIVE -> gestureProgress
527                 GestureState.ACTIVE,
528                 GestureState.FLUNG,
529                 GestureState.COMMITTED -> 1f
530                 GestureState.CANCELLED,
531                 GestureState.GONE -> 0f
532             }
533 
534         val indicator =
535             when (currentState) {
536                 GestureState.ENTRY -> params.entryIndicator
537                 GestureState.INACTIVE -> params.preThresholdIndicator
538                 GestureState.ACTIVE -> params.activeIndicator
539                 else -> params.preThresholdIndicator
540             }
541 
542         strokeAlphaProgress?.let { progress ->
543             indicator.arrowDimens.alphaSpring
544                 ?.get(progress)
545                 ?.takeIf { it.isNewState }
546                 ?.let { mView.popArrowAlpha(0f, it.value) }
547         }
548     }
549 
550     private fun setVerticalTranslation(yOffset: Float) {
551         val yTranslation = abs(yOffset)
552         val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f
553         val rubberbandAmount = 15f
554         val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount))
555         val yPosition =
556             params.verticalTranslationInterpolator.getInterpolation(yProgress) *
557                 maxYOffset *
558                 sign(yOffset)
559         mView.animateVertically(yPosition)
560     }
561 
562     /**
563      * Tracks the relative position of the drag from the time after the arrow is activated until the
564      * arrow is fully stretched (between 0.0 - 1.0f)
565      */
566     private fun fullScreenProgress(xTranslation: Float): Float {
567         val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold
568         return MathUtils.saturate(progress)
569     }
570 
571     /**
572      * Tracks the relative position of the drag from the entry until the threshold where the arrow
573      * activates (between 0.0 - 1.0f)
574      */
575     private fun staticThresholdProgress(xTranslation: Float): Float {
576         return MathUtils.saturate(xTranslation / params.staticTriggerThreshold)
577     }
578 
579     private fun reactivationThresholdProgress(totalTouchDelta: Float): Float {
580         return MathUtils.saturate(totalTouchDelta / params.reactivationTriggerThreshold)
581     }
582 
583     private fun stretchActiveBackIndicator(progress: Float) {
584         mView.setStretch(
585             horizontalTranslationStretchAmount =
586                 params.horizontalTranslationInterpolator.getInterpolation(progress),
587             arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
588             backgroundWidthStretchAmount =
589                 params.activeWidthInterpolator.getInterpolation(progress),
590             backgroundAlphaStretchAmount = 1f,
591             backgroundHeightStretchAmount = 1f,
592             arrowAlphaStretchAmount = 1f,
593             edgeCornerStretchAmount = 1f,
594             farCornerStretchAmount = 1f,
595             fullyStretchedDimens = params.fullyStretchedIndicator
596         )
597     }
598 
599     private fun stretchEntryBackIndicator(progress: Float) {
600         mView.setStretch(
601             horizontalTranslationStretchAmount = 0f,
602             arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
603             backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress),
604             backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
605             backgroundAlphaStretchAmount = 1f,
606             arrowAlphaStretchAmount =
607                 params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f,
608             edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
609             farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
610             fullyStretchedDimens = params.preThresholdIndicator
611         )
612     }
613 
614     private var previousPreThresholdWidthInterpolator = params.entryWidthInterpolator
615 
616     private fun preThresholdWidthStretchAmount(progress: Float): Float {
617         val interpolator = run {
618             val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop
619             if (isPastSlop) {
620                     if (totalTouchDeltaInactive > 0) {
621                         params.entryWidthInterpolator
622                     } else {
623                         params.entryWidthTowardsEdgeInterpolator
624                     }
625                 } else {
626                     previousPreThresholdWidthInterpolator
627                 }
628                 .also { previousPreThresholdWidthInterpolator = it }
629         }
630         return interpolator.getInterpolation(progress).coerceAtLeast(0f)
631     }
632 
633     private fun stretchInactiveBackIndicator(progress: Float) {
634         mView.setStretch(
635             horizontalTranslationStretchAmount = 0f,
636             arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress),
637             backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress),
638             backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress),
639             backgroundAlphaStretchAmount = 1f,
640             arrowAlphaStretchAmount =
641                 params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value
642                     ?: 0f,
643             edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress),
644             farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress),
645             fullyStretchedDimens = params.preThresholdIndicator
646         )
647     }
648 
649     override fun onDestroy() {
650         cancelFailsafe()
651         windowManager.removeView(mView)
652     }
653 
654     override fun setIsLeftPanel(isLeftPanel: Boolean) {
655         mView.isLeftPanel = isLeftPanel
656         layoutParams.gravity =
657             if (isLeftPanel) {
658                 Gravity.LEFT or Gravity.TOP
659             } else {
660                 Gravity.RIGHT or Gravity.TOP
661             }
662     }
663 
664     override fun setInsets(insetLeft: Int, insetRight: Int) = Unit
665 
666     override fun setBackCallback(callback: NavigationEdgeBackPlugin.BackCallback) {
667         backCallback = callback
668     }
669 
670     override fun setLayoutParams(layoutParams: WindowManager.LayoutParams) {
671         this.layoutParams = layoutParams
672         windowManager.addView(mView, layoutParams)
673     }
674 
675     private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean {
676         val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX
677         val flingVelocity =
678             velocityTracker?.run {
679                 computeCurrentVelocity(PX_PER_SEC)
680                 xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1)
681             } ?: 0f
682         val isPastFlingVelocityThreshold =
683             flingVelocity > viewConfiguration.scaledMinimumFlingVelocity
684         return flingDistance > minFlingDistance && isPastFlingVelocityThreshold
685     }
686 
687     private fun isPastThresholdToActive(
688         isPastThreshold: Boolean,
689         delay: Float? = null,
690         dynamicDelay: () -> Float = { delay ?: 0F }
691     ): Boolean {
692         val resetValue = 0L
693         val isPastThresholdForFirstTime = pastThresholdWhileEntryOrInactiveTime == resetValue
694 
695         if (!isPastThreshold) {
696             pastThresholdWhileEntryOrInactiveTime = resetValue
697             return false
698         }
699 
700         if (isPastThresholdForFirstTime) {
701             pastThresholdWhileEntryOrInactiveTime = systemClock.uptimeMillis()
702             entryToActiveDelay = dynamicDelay()
703         }
704         val timePastThreshold = systemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime
705 
706         return timePastThreshold > entryToActiveDelay
707     }
708 
709     private fun playWithBackgroundWidthAnimation(
710         onEnd: DelayedOnAnimationEndListener,
711         delay: Long = 0L
712     ) {
713         if (delay == 0L) {
714             updateRestingArrowDimens()
715             if (!mView.addAnimationEndListener(mView.backgroundWidth, onEnd)) {
716                 scheduleFailsafe()
717             }
718         } else {
719             mainHandler.postDelayed(delay) { playWithBackgroundWidthAnimation(onEnd, delay = 0L) }
720         }
721     }
722 
723     private fun updateYStartPosition(touchY: Float) {
724         var yPosition = touchY - params.fingerOffset
725         yPosition = max(yPosition, params.minArrowYPosition.toFloat())
726         yPosition -= layoutParams.height / 2.0f
727         layoutParams.y = MathUtils.constrain(yPosition.toInt(), 0, displaySize.y)
728     }
729 
730     override fun setDisplaySize(displaySize: Point) {
731         this.displaySize.set(displaySize.x, displaySize.y)
732         fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold)
733     }
734 
735     /** Updates resting arrow and background size not accounting for stretch */
736     private fun updateRestingArrowDimens() {
737         when (currentState) {
738             GestureState.GONE,
739             GestureState.ENTRY -> {
740                 mView.setSpring(
741                     arrowLength = params.entryIndicator.arrowDimens.lengthSpring,
742                     arrowHeight = params.entryIndicator.arrowDimens.heightSpring,
743                     scale = params.entryIndicator.scaleSpring,
744                     verticalTranslation = params.entryIndicator.verticalTranslationSpring,
745                     horizontalTranslation = params.entryIndicator.horizontalTranslationSpring,
746                     backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring,
747                     backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring,
748                     backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring,
749                     backgroundEdgeCornerRadius =
750                         params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring,
751                     backgroundFarCornerRadius =
752                         params.entryIndicator.backgroundDimens.farCornerRadiusSpring,
753                 )
754             }
755             GestureState.INACTIVE -> {
756                 mView.setSpring(
757                     arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring,
758                     arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring,
759                     horizontalTranslation =
760                         params.preThresholdIndicator.horizontalTranslationSpring,
761                     scale = params.preThresholdIndicator.scaleSpring,
762                     backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring,
763                     backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring,
764                     backgroundEdgeCornerRadius =
765                         params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring,
766                     backgroundFarCornerRadius =
767                         params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring,
768                 )
769             }
770             GestureState.ACTIVE -> {
771                 mView.setSpring(
772                     arrowLength = params.activeIndicator.arrowDimens.lengthSpring,
773                     arrowHeight = params.activeIndicator.arrowDimens.heightSpring,
774                     scale = params.activeIndicator.scaleSpring,
775                     horizontalTranslation = params.activeIndicator.horizontalTranslationSpring,
776                     backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring,
777                     backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring,
778                     backgroundEdgeCornerRadius =
779                         params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring,
780                     backgroundFarCornerRadius =
781                         params.activeIndicator.backgroundDimens.farCornerRadiusSpring,
782                 )
783             }
784             GestureState.FLUNG -> {
785                 mView.setSpring(
786                     arrowLength = params.flungIndicator.arrowDimens.lengthSpring,
787                     arrowHeight = params.flungIndicator.arrowDimens.heightSpring,
788                     backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring,
789                     backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring,
790                     backgroundEdgeCornerRadius =
791                         params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring,
792                     backgroundFarCornerRadius =
793                         params.flungIndicator.backgroundDimens.farCornerRadiusSpring,
794                 )
795             }
796             GestureState.COMMITTED -> {
797                 mView.setSpring(
798                     arrowLength = params.committedIndicator.arrowDimens.lengthSpring,
799                     arrowHeight = params.committedIndicator.arrowDimens.heightSpring,
800                     scale = params.committedIndicator.scaleSpring,
801                     backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring,
802                     backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring,
803                     backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring,
804                     backgroundEdgeCornerRadius =
805                         params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring,
806                     backgroundFarCornerRadius =
807                         params.committedIndicator.backgroundDimens.farCornerRadiusSpring,
808                 )
809             }
810             GestureState.CANCELLED -> {
811                 mView.setSpring(
812                     backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring
813                 )
814             }
815             else -> {}
816         }
817 
818         mView.setRestingDimens(
819             animate =
820                 !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED),
821             restingParams =
822                 EdgePanelParams.BackIndicatorDimens(
823                     scale =
824                         when (currentState) {
825                             GestureState.ACTIVE,
826                             GestureState.FLUNG -> params.activeIndicator.scale
827                             GestureState.COMMITTED -> params.committedIndicator.scale
828                             else -> params.preThresholdIndicator.scale
829                         },
830                     scalePivotX =
831                         when (currentState) {
832                             GestureState.GONE,
833                             GestureState.ENTRY,
834                             GestureState.INACTIVE,
835                             GestureState.CANCELLED -> params.preThresholdIndicator.scalePivotX
836                             GestureState.ACTIVE -> params.activeIndicator.scalePivotX
837                             GestureState.FLUNG,
838                             GestureState.COMMITTED -> params.committedIndicator.scalePivotX
839                         },
840                     horizontalTranslation =
841                         when (currentState) {
842                             GestureState.GONE -> {
843                                 params.activeIndicator.backgroundDimens.width?.times(-1)
844                             }
845                             GestureState.ENTRY,
846                             GestureState.INACTIVE -> params.entryIndicator.horizontalTranslation
847                             GestureState.FLUNG -> params.activeIndicator.horizontalTranslation
848                             GestureState.ACTIVE -> params.activeIndicator.horizontalTranslation
849                             GestureState.CANCELLED -> {
850                                 params.cancelledIndicator.horizontalTranslation
851                             }
852                             else -> null
853                         },
854                     arrowDimens =
855                         when (currentState) {
856                             GestureState.GONE,
857                             GestureState.ENTRY,
858                             GestureState.INACTIVE -> params.entryIndicator.arrowDimens
859                             GestureState.ACTIVE -> params.activeIndicator.arrowDimens
860                             GestureState.FLUNG -> params.flungIndicator.arrowDimens
861                             GestureState.COMMITTED -> params.committedIndicator.arrowDimens
862                             GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens
863                         },
864                     backgroundDimens =
865                         when (currentState) {
866                             GestureState.GONE,
867                             GestureState.ENTRY,
868                             GestureState.INACTIVE -> params.entryIndicator.backgroundDimens
869                             GestureState.ACTIVE -> params.activeIndicator.backgroundDimens
870                             GestureState.FLUNG -> params.activeIndicator.backgroundDimens
871                             GestureState.COMMITTED -> params.committedIndicator.backgroundDimens
872                             GestureState.CANCELLED -> params.cancelledIndicator.backgroundDimens
873                         }
874                 )
875         )
876     }
877 
878     /**
879      * Update arrow state. If state has not changed, this is a no-op.
880      *
881      * Transitioning to active/inactive will indicate whether or not releasing touch will trigger
882      * the back action.
883      */
884     private fun updateArrowState(newState: GestureState, force: Boolean = false) {
885         if (!force && currentState == newState) return
886 
887         previousState = currentState
888         currentState = newState
889 
890         // First, update the jank tracker
891         when (currentState) {
892             GestureState.ENTRY -> {
893                 interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW)
894                 interactionJankMonitor.begin(mView, Cuj.CUJ_BACK_PANEL_ARROW)
895             }
896             GestureState.GONE -> interactionJankMonitor.end(Cuj.CUJ_BACK_PANEL_ARROW)
897             else -> {}
898         }
899 
900         when (currentState) {
901             GestureState.CANCELLED -> {
902                 backCallback.cancelBack()
903             }
904             GestureState.FLUNG,
905             GestureState.COMMITTED -> {
906                 // When flung, trigger back immediately but don't fire again
907                 // once state resolves to committed.
908                 if (previousState != GestureState.FLUNG) backCallback.triggerBack()
909             }
910             GestureState.ENTRY,
911             GestureState.INACTIVE -> {
912                 backCallback.setTriggerBack(false)
913             }
914             GestureState.ACTIVE -> {
915                 backCallback.setTriggerBack(true)
916             }
917             GestureState.GONE -> {}
918         }
919 
920         when (currentState) {
921             // Transitioning to GONE never animates since the arrow is (presumably) already off the
922             // screen
923             GestureState.GONE -> {
924                 updateRestingArrowDimens()
925                 mView.isVisible = false
926             }
927             GestureState.ENTRY -> {
928                 mView.isVisible = true
929 
930                 updateRestingArrowDimens()
931                 gestureEntryTime = systemClock.uptimeMillis()
932             }
933             GestureState.ACTIVE -> {
934                 previousXTranslationOnActiveOffset = previousXTranslation
935                 updateRestingArrowDimens()
936                 performActivatedHapticFeedback()
937                 val popVelocity =
938                     if (previousState == GestureState.INACTIVE) {
939                         POP_ON_INACTIVE_TO_ACTIVE_VELOCITY
940                     } else {
941                         POP_ON_ENTRY_TO_ACTIVE_VELOCITY
942                     }
943                 mView.popOffEdge(popVelocity)
944             }
945             GestureState.INACTIVE -> {
946                 gestureInactiveTime = systemClock.uptimeMillis()
947 
948                 // Typically entering INACTIVE means
949                 // totalTouchDelta <= deactivationSwipeTriggerThreshold
950                 // but because we can also independently enter this state
951                 // if touch Y >> touch X, we force it to deactivationSwipeTriggerThreshold
952                 // so that gesture progress in this state is consistent regardless of entry
953                 totalTouchDeltaInactive = params.deactivationTriggerThreshold
954 
955                 mView.popOffEdge(POP_ON_INACTIVE_VELOCITY)
956 
957                 performDeactivatedHapticFeedback()
958                 updateRestingArrowDimens()
959             }
960             GestureState.FLUNG -> {
961                 // Typically a vibration is only played while transitioning to ACTIVE. However there
962                 // are instances where a fling to trigger back occurs while not in that state.
963                 // (e.g. A fling is detected before crossing the trigger threshold.)
964                 if (previousState != GestureState.ACTIVE) {
965                     performActivatedHapticFeedback()
966                 }
967                 mainHandler.postDelayed(POP_ON_FLING_DELAY) {
968                     mView.popScale(POP_ON_FLING_VELOCITY)
969                 }
970                 mainHandler.postDelayed(
971                     onEndSetCommittedStateListener.runnable,
972                     MIN_DURATION_FLING_ANIMATION
973                 )
974                 updateRestingArrowDimens()
975             }
976             GestureState.COMMITTED -> {
977                 // In most cases, animating between states is handled via `updateRestingArrowDimens`
978                 // which plays an animation immediately upon state change. Some animations however
979                 // occur after a delay upon state change and these animations may be independent
980                 // or non-sequential from the state change animation. `postDelayed` is used to
981                 // manually play these kinds of animations in parallel.
982                 if (previousState == GestureState.FLUNG) {
983                     updateRestingArrowDimens()
984                     mainHandler.postDelayed(
985                         onEndSetGoneStateListener.runnable,
986                         MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION
987                     )
988                 } else {
989                     mView.popScale(POP_ON_COMMITTED_VELOCITY)
990                     mainHandler.postDelayed(
991                         onAlphaEndSetGoneStateListener.runnable,
992                         MIN_DURATION_COMMITTED_ANIMATION
993                     )
994                 }
995             }
996             GestureState.CANCELLED -> {
997                 val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry)
998                 playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay)
999 
1000                 val springForceOnCancelled =
1001                     params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value
1002                 mView.popArrowAlpha(0f, springForceOnCancelled)
1003             }
1004         }
1005     }
1006 
1007     private fun performDeactivatedHapticFeedback() {
1008         vibratorHelper.performHapticFeedback(
1009             mView,
1010             HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE
1011         )
1012     }
1013 
1014     private fun performActivatedHapticFeedback() {
1015         vibratorHelper.performHapticFeedback(
1016             mView,
1017             HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE
1018         )
1019     }
1020 
1021     private fun convertVelocityToAnimationFactor(
1022         valueOnFastVelocity: Float,
1023         valueOnSlowVelocity: Float,
1024         fastVelocityBound: Float = 1f,
1025         slowVelocityBound: Float = 0.5f,
1026     ): Float {
1027         val factor =
1028             velocityTracker?.run {
1029                 computeCurrentVelocity(PX_PER_MS)
1030                 MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity))
1031             } ?: valueOnFastVelocity
1032 
1033         return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor)
1034     }
1035 
1036     private fun scheduleFailsafe() {
1037         if (!ENABLE_FAILSAFE) return
1038         cancelFailsafe()
1039         if (DEBUG) Log.d(TAG, "scheduleFailsafe")
1040         mainHandler.postDelayed(failsafeRunnable, FAILSAFE_DELAY_MS)
1041     }
1042 
1043     private fun cancelFailsafe() {
1044         if (DEBUG) Log.d(TAG, "cancelFailsafe")
1045         mainHandler.removeCallbacks(failsafeRunnable)
1046     }
1047 
1048     private fun onFailsafe() {
1049         if (DEBUG) Log.d(TAG, "onFailsafe")
1050         updateArrowState(GestureState.GONE, force = true)
1051     }
1052 
1053     override fun dump(pw: PrintWriter) {
1054         pw.println("$TAG:")
1055         pw.println("  currentState=$currentState")
1056         pw.println("  isLeftPanel=${mView.isLeftPanel}")
1057     }
1058 
1059     @VisibleForTesting
1060     internal fun getBackPanelView(): BackPanel {
1061         return mView
1062     }
1063 
1064     init {
1065         if (DEBUG)
1066             mView.drawDebugInfo = { canvas ->
1067                 val preProgress = staticThresholdProgress(previousXTranslation) * 100
1068                 val postProgress = fullScreenProgress(previousXTranslation) * 100
1069                 val debugStrings =
1070                     listOf(
1071                         "$currentState",
1072                         "startX=$startX",
1073                         "startY=$startY",
1074                         "xDelta=${"%.1f".format(totalTouchDeltaActive)}",
1075                         "xTranslation=${"%.1f".format(previousXTranslation)}",
1076                         "pre=${"%.0f".format(preProgress)}%",
1077                         "post=${"%.0f".format(postProgress)}%"
1078                     )
1079                 val debugPaint = Paint().apply { color = Color.WHITE }
1080                 val debugInfoBottom = debugStrings.size * 32f + 4f
1081                 canvas.drawRect(
1082                     4f,
1083                     4f,
1084                     canvas.width.toFloat(),
1085                     debugStrings.size * 32f + 4f,
1086                     debugPaint
1087                 )
1088                 debugPaint.apply {
1089                     color = Color.BLACK
1090                     textSize = 32f
1091                 }
1092                 var offset = 32f
1093                 for (debugText in debugStrings) {
1094                     canvas.drawText(debugText, 10f, offset, debugPaint)
1095                     offset += 32f
1096                 }
1097                 debugPaint.apply {
1098                     color = Color.RED
1099                     style = Paint.Style.STROKE
1100                     strokeWidth = 4f
1101                 }
1102                 val canvasWidth = canvas.width.toFloat()
1103                 val canvasHeight = canvas.height.toFloat()
1104                 canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint)
1105 
1106                 fun drawVerticalLine(x: Float, color: Int) {
1107                     debugPaint.color = color
1108                     val x = if (mView.isLeftPanel) x else canvasWidth - x
1109                     canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint)
1110                 }
1111 
1112                 drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE)
1113                 drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE)
1114                 drawVerticalLine(x = startX, color = Color.GREEN)
1115                 drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY)
1116             }
1117     }
1118 }
1119 
1120 /**
1121  * In addition to a typical step function which returns one or two values based on a threshold,
1122  * `Step` also gracefully handles quick changes in input near the threshold value that would
1123  * typically result in the output rapidly changing.
1124  *
1125  * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or
1126  * opaque. Using a typical Step function, this would resulting in a flickering appearance as the
1127  * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so
1128  * it cannot be easily crossed again with small changes in touch events.
1129  */
1130 class Step<T>(
1131     private val threshold: Float,
1132     private val factor: Float = 1.1f,
1133     private val postThreshold: T,
1134     private val preThreshold: T
1135 ) {
1136 
1137     data class Value<T>(val value: T, val isNewState: Boolean)
1138 
1139     private val lowerFactor = 2 - factor
1140 
1141     private lateinit var startValue: Value<T>
1142     private lateinit var previousValue: Value<T>
1143     private var hasCrossedUpperBoundAtLeastOnce = false
1144     private var progress: Float = 0f
1145 
1146     init {
1147         reset()
1148     }
1149 
resetnull1150     fun reset() {
1151         hasCrossedUpperBoundAtLeastOnce = false
1152         progress = 0f
1153         startValue = Value(preThreshold, false)
1154         previousValue = startValue
1155     }
1156 
getnull1157     fun get(progress: Float): Value<T> {
1158         this.progress = progress
1159 
1160         val hasCrossedUpperBound = progress > threshold * factor
1161         val hasCrossedLowerBound = progress > threshold * lowerFactor
1162 
1163         return when {
1164             hasCrossedUpperBound && !hasCrossedUpperBoundAtLeastOnce -> {
1165                 hasCrossedUpperBoundAtLeastOnce = true
1166                 Value(postThreshold, true)
1167             }
1168             hasCrossedLowerBound -> previousValue.copy(isNewState = false)
1169             hasCrossedUpperBoundAtLeastOnce -> {
1170                 hasCrossedUpperBoundAtLeastOnce = false
1171                 Value(preThreshold, true)
1172             }
1173             else -> startValue
1174         }.also { previousValue = it }
1175     }
1176 }
1177