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