Loading packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +29 −8 Original line number Diff line number Diff line Loading @@ -21,19 +21,20 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Point import android.os.Handler import android.os.SystemClock import android.util.Log import android.util.MathUtils import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.VelocityTracker import android.view.View import android.view.ViewConfiguration import android.view.WindowManager import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main Loading @@ -41,6 +42,7 @@ import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.ViewController import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject import kotlin.math.abs Loading Loading @@ -84,6 +86,7 @@ internal constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, @Main private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, Loading @@ -102,6 +105,7 @@ internal constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, @Main private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, Loading @@ -115,6 +119,7 @@ internal constructor( windowManager, viewConfiguration, mainHandler, systemClock, vibratorHelper, configurationController, latencyTracker, Loading Loading @@ -158,9 +163,9 @@ internal constructor( private var gestureInactiveTime = 0L private val elapsedTimeSinceInactive get() = SystemClock.uptimeMillis() - gestureInactiveTime get() = systemClock.uptimeMillis() - gestureInactiveTime private val elapsedTimeSinceEntry get() = SystemClock.uptimeMillis() - gestureEntryTime get() = systemClock.uptimeMillis() - gestureEntryTime private var pastThresholdWhileEntryOrInactiveTime = 0L private var entryToActiveDelay = 0F Loading @@ -178,7 +183,7 @@ internal constructor( // Distance in pixels a drag can be considered for a fling event private var minFlingDistance = 0 private val failsafeRunnable = Runnable { onFailsafe() } internal val failsafeRunnable = Runnable { onFailsafe() } internal enum class GestureState { /* Arrow is off the screen and invisible */ Loading Loading @@ -370,6 +375,7 @@ internal constructor( // Receiving a CANCEL implies that something else intercepted // the gesture, i.e., the user did not cancel their gesture. // Therefore, disappear immediately, with minimum fanfare. interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) updateArrowState(GestureState.GONE) velocityTracker = null } Loading Loading @@ -692,10 +698,10 @@ internal constructor( } if (isPastThresholdForFirstTime) { pastThresholdWhileEntryOrInactiveTime = SystemClock.uptimeMillis() pastThresholdWhileEntryOrInactiveTime = systemClock.uptimeMillis() entryToActiveDelay = dynamicDelay() } val timePastThreshold = SystemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime val timePastThreshold = systemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime return timePastThreshold > entryToActiveDelay } Loading Loading @@ -881,6 +887,16 @@ internal constructor( previousState = currentState currentState = newState // First, update the jank tracker when (currentState) { GestureState.ENTRY -> { interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) interactionJankMonitor.begin(mView, Cuj.CUJ_BACK_PANEL_ARROW) } GestureState.GONE -> interactionJankMonitor.end(Cuj.CUJ_BACK_PANEL_ARROW) else -> {} } when (currentState) { GestureState.CANCELLED -> { backCallback.cancelBack() Loading Loading @@ -912,7 +928,7 @@ internal constructor( mView.isVisible = true updateRestingArrowDimens() gestureEntryTime = SystemClock.uptimeMillis() gestureEntryTime = systemClock.uptimeMillis() } GestureState.ACTIVE -> { previousXTranslationOnActiveOffset = previousXTranslation Loading @@ -927,7 +943,7 @@ internal constructor( mView.popOffEdge(popVelocity) } GestureState.INACTIVE -> { gestureInactiveTime = SystemClock.uptimeMillis() gestureInactiveTime = systemClock.uptimeMillis() // Typically entering INACTIVE means // totalTouchDelta <= deactivationSwipeTriggerThreshold Loading Loading @@ -1041,6 +1057,11 @@ internal constructor( pw.println(" isLeftPanel=${mView.isLeftPanel}") } @VisibleForTesting internal fun getBackPanelView(): BackPanel { return mView } init { if (DEBUG) mView.drawDebugInfo = { canvas -> Loading packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.systemui.navigationbar.gestural import android.os.Handler import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.HapticFeedbackConstants Loading @@ -28,6 +27,7 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase import com.android.systemui.jank.interactionJankMonitor Loading @@ -35,6 +35,7 @@ import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test Loading @@ -43,6 +44,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -55,6 +57,7 @@ class BackPanelControllerTest : SysuiTestCase() { } private val kosmos = testKosmos() private lateinit var mBackPanelController: BackPanelController private lateinit var systemClock: FakeSystemClock private lateinit var testableLooper: TestableLooper private var triggerThreshold: Float = 0.0f private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop Loading @@ -69,12 +72,15 @@ class BackPanelControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) systemClock = FakeSystemClock() mBackPanelController = BackPanelController( context, windowManager, ViewConfiguration.get(context), Handler.createAsync(checkNotNull(Looper.myLooper())), Handler.createAsync(testableLooper.looper), systemClock, vibratorHelper, configurationController, latencyTracker, Loading @@ -83,7 +89,6 @@ class BackPanelControllerTest : SysuiTestCase() { mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) mBackPanelController.setIsLeftPanel(true) testableLooper = TestableLooper.get(this) triggerThreshold = mBackPanelController.params.staticTriggerThreshold } Loading @@ -103,6 +108,7 @@ class BackPanelControllerTest : SysuiTestCase() { assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.GONE) verify(interactionJankMonitor, never()).begin(any()) } @Test Loading @@ -110,23 +116,37 @@ class BackPanelControllerTest : SysuiTestCase() { startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ENTRY) verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW) verify(interactionJankMonitor) .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW) // Move again to cross the back trigger threshold continueTouch(START_X + touchSlop + triggerThreshold + 1) // Wait threshold duration and hold touch past trigger threshold Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) continueTouch(START_X + touchSlop + triggerThreshold + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ACTIVE) verify(backCallback).setTriggerBack(true) testableLooper.moveTimeForward(100) testableLooper.processAllMessages() moveTimeForward(100) verify(vibratorHelper) .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.COMMITTED) verify(backCallback).triggerBack() // Because the Handler that is typically used for transitioning the arrow state from // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation, // there is no way to meaningfully test that the state becomes GONE and that the tracked // jank interaction is ended. So instead, manually trigger the failsafe, which does // the same thing: mBackPanelController.failsafeRunnable.run() assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.GONE) verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW) } @Test Loading @@ -134,19 +154,22 @@ class BackPanelControllerTest : SysuiTestCase() { startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ENTRY) // Move again to cross the back trigger threshold continueTouch( START_X + touchSlop + triggerThreshold - mBackPanelController.params.deactivationTriggerThreshold ) // Wait threshold duration and hold touch before trigger threshold Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) continueTouch( START_X + touchSlop + triggerThreshold - mBackPanelController.params.deactivationTriggerThreshold ) clearInvocations(backCallback) Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) // Move in the opposite direction to cross the deactivation threshold and cancel back continueTouch(START_X) Loading Loading @@ -175,4 +198,10 @@ class BackPanelControllerTest : SysuiTestCase() { private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent { return MotionEvent.obtain(0L, 0L, action, x, y, 0) } private fun moveTimeForward(millis: Long) { systemClock.advanceTime(millis) testableLooper.moveTimeForward(millis) testableLooper.processAllMessages() } } Loading
packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +29 −8 Original line number Diff line number Diff line Loading @@ -21,19 +21,20 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Point import android.os.Handler import android.os.SystemClock import android.util.Log import android.util.MathUtils import android.view.Gravity import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.VelocityTracker import android.view.View import android.view.ViewConfiguration import android.view.WindowManager import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main Loading @@ -41,6 +42,7 @@ import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.ViewController import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import javax.inject.Inject import kotlin.math.abs Loading Loading @@ -84,6 +86,7 @@ internal constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, @Main private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, Loading @@ -102,6 +105,7 @@ internal constructor( private val windowManager: WindowManager, private val viewConfiguration: ViewConfiguration, @Main private val mainHandler: Handler, private val systemClock: SystemClock, private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, Loading @@ -115,6 +119,7 @@ internal constructor( windowManager, viewConfiguration, mainHandler, systemClock, vibratorHelper, configurationController, latencyTracker, Loading Loading @@ -158,9 +163,9 @@ internal constructor( private var gestureInactiveTime = 0L private val elapsedTimeSinceInactive get() = SystemClock.uptimeMillis() - gestureInactiveTime get() = systemClock.uptimeMillis() - gestureInactiveTime private val elapsedTimeSinceEntry get() = SystemClock.uptimeMillis() - gestureEntryTime get() = systemClock.uptimeMillis() - gestureEntryTime private var pastThresholdWhileEntryOrInactiveTime = 0L private var entryToActiveDelay = 0F Loading @@ -178,7 +183,7 @@ internal constructor( // Distance in pixels a drag can be considered for a fling event private var minFlingDistance = 0 private val failsafeRunnable = Runnable { onFailsafe() } internal val failsafeRunnable = Runnable { onFailsafe() } internal enum class GestureState { /* Arrow is off the screen and invisible */ Loading Loading @@ -370,6 +375,7 @@ internal constructor( // Receiving a CANCEL implies that something else intercepted // the gesture, i.e., the user did not cancel their gesture. // Therefore, disappear immediately, with minimum fanfare. interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) updateArrowState(GestureState.GONE) velocityTracker = null } Loading Loading @@ -692,10 +698,10 @@ internal constructor( } if (isPastThresholdForFirstTime) { pastThresholdWhileEntryOrInactiveTime = SystemClock.uptimeMillis() pastThresholdWhileEntryOrInactiveTime = systemClock.uptimeMillis() entryToActiveDelay = dynamicDelay() } val timePastThreshold = SystemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime val timePastThreshold = systemClock.uptimeMillis() - pastThresholdWhileEntryOrInactiveTime return timePastThreshold > entryToActiveDelay } Loading Loading @@ -881,6 +887,16 @@ internal constructor( previousState = currentState currentState = newState // First, update the jank tracker when (currentState) { GestureState.ENTRY -> { interactionJankMonitor.cancel(Cuj.CUJ_BACK_PANEL_ARROW) interactionJankMonitor.begin(mView, Cuj.CUJ_BACK_PANEL_ARROW) } GestureState.GONE -> interactionJankMonitor.end(Cuj.CUJ_BACK_PANEL_ARROW) else -> {} } when (currentState) { GestureState.CANCELLED -> { backCallback.cancelBack() Loading Loading @@ -912,7 +928,7 @@ internal constructor( mView.isVisible = true updateRestingArrowDimens() gestureEntryTime = SystemClock.uptimeMillis() gestureEntryTime = systemClock.uptimeMillis() } GestureState.ACTIVE -> { previousXTranslationOnActiveOffset = previousXTranslation Loading @@ -927,7 +943,7 @@ internal constructor( mView.popOffEdge(popVelocity) } GestureState.INACTIVE -> { gestureInactiveTime = SystemClock.uptimeMillis() gestureInactiveTime = systemClock.uptimeMillis() // Typically entering INACTIVE means // totalTouchDelta <= deactivationSwipeTriggerThreshold Loading Loading @@ -1041,6 +1057,11 @@ internal constructor( pw.println(" isLeftPanel=${mView.isLeftPanel}") } @VisibleForTesting internal fun getBackPanelView(): BackPanel { return mView } init { if (DEBUG) mView.drawDebugInfo = { canvas -> Loading
packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +37 −8 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.systemui.navigationbar.gestural import android.os.Handler import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.HapticFeedbackConstants Loading @@ -28,6 +27,7 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase import com.android.systemui.jank.interactionJankMonitor Loading @@ -35,6 +35,7 @@ import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test Loading @@ -43,6 +44,7 @@ import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations Loading @@ -55,6 +57,7 @@ class BackPanelControllerTest : SysuiTestCase() { } private val kosmos = testKosmos() private lateinit var mBackPanelController: BackPanelController private lateinit var systemClock: FakeSystemClock private lateinit var testableLooper: TestableLooper private var triggerThreshold: Float = 0.0f private val touchSlop = ViewConfiguration.get(context).scaledEdgeSlop Loading @@ -69,12 +72,15 @@ class BackPanelControllerTest : SysuiTestCase() { @Before fun setup() { MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) systemClock = FakeSystemClock() mBackPanelController = BackPanelController( context, windowManager, ViewConfiguration.get(context), Handler.createAsync(checkNotNull(Looper.myLooper())), Handler.createAsync(testableLooper.looper), systemClock, vibratorHelper, configurationController, latencyTracker, Loading @@ -83,7 +89,6 @@ class BackPanelControllerTest : SysuiTestCase() { mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) mBackPanelController.setIsLeftPanel(true) testableLooper = TestableLooper.get(this) triggerThreshold = mBackPanelController.params.staticTriggerThreshold } Loading @@ -103,6 +108,7 @@ class BackPanelControllerTest : SysuiTestCase() { assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.GONE) verify(interactionJankMonitor, never()).begin(any()) } @Test Loading @@ -110,23 +116,37 @@ class BackPanelControllerTest : SysuiTestCase() { startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ENTRY) verify(interactionJankMonitor).cancel(Cuj.CUJ_BACK_PANEL_ARROW) verify(interactionJankMonitor) .begin(mBackPanelController.getBackPanelView(), Cuj.CUJ_BACK_PANEL_ARROW) // Move again to cross the back trigger threshold continueTouch(START_X + touchSlop + triggerThreshold + 1) // Wait threshold duration and hold touch past trigger threshold Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) continueTouch(START_X + touchSlop + triggerThreshold + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ACTIVE) verify(backCallback).setTriggerBack(true) testableLooper.moveTimeForward(100) testableLooper.processAllMessages() moveTimeForward(100) verify(vibratorHelper) .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.COMMITTED) verify(backCallback).triggerBack() // Because the Handler that is typically used for transitioning the arrow state from // COMMITTED to GONE is used as an animation-end-listener on a SpringAnimation, // there is no way to meaningfully test that the state becomes GONE and that the tracked // jank interaction is ended. So instead, manually trigger the failsafe, which does // the same thing: mBackPanelController.failsafeRunnable.run() assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.GONE) verify(interactionJankMonitor).end(Cuj.CUJ_BACK_PANEL_ARROW) } @Test Loading @@ -134,19 +154,22 @@ class BackPanelControllerTest : SysuiTestCase() { startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) assertThat(mBackPanelController.currentState) .isEqualTo(BackPanelController.GestureState.ENTRY) // Move again to cross the back trigger threshold continueTouch( START_X + touchSlop + triggerThreshold - mBackPanelController.params.deactivationTriggerThreshold ) // Wait threshold duration and hold touch before trigger threshold Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) moveTimeForward((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) continueTouch( START_X + touchSlop + triggerThreshold - mBackPanelController.params.deactivationTriggerThreshold ) clearInvocations(backCallback) Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) moveTimeForward(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) // Move in the opposite direction to cross the deactivation threshold and cancel back continueTouch(START_X) Loading Loading @@ -175,4 +198,10 @@ class BackPanelControllerTest : SysuiTestCase() { private fun createMotionEvent(action: Int, x: Float, y: Float): MotionEvent { return MotionEvent.obtain(0L, 0L, action, x, y, 0) } private fun moveTimeForward(millis: Long) { systemClock.advanceTime(millis) testableLooper.moveTimeForward(millis) testableLooper.processAllMessages() } }