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