Loading packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +3 −5 Original line number Diff line number Diff line Loading @@ -60,14 +60,12 @@ fun createUnfoldTransitionProgressProvider( hingeAngleProvider, screenStatusProvider, deviceStateManager, mainExecutor mainExecutor, mainHandler ) val unfoldTransitionProgressProvider = if (config.isHingeAngleEnabled) { PhysicsBasedUnfoldTransitionProgressProvider( mainHandler, foldStateProvider ) PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider) } else { FixedTimingTransitionProgressProvider(foldStateProvider) } Loading packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +8 −19 Original line number Diff line number Diff line Loading @@ -15,7 +15,6 @@ */ package com.android.systemui.unfold.progress import android.os.Handler import android.util.Log import android.util.MathUtils.saturate import androidx.dynamicanimation.animation.DynamicAnimation Loading @@ -24,9 +23,10 @@ import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.updates.FOLD_UPDATE_ABORTED import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate Loading @@ -39,7 +39,6 @@ import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener * - doesn't handle postures */ internal class PhysicsBasedUnfoldTransitionProgressProvider( private val handler: Handler, private val foldStateProvider: FoldStateProvider ) : UnfoldTransitionProgressProvider, Loading @@ -51,8 +50,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) } private val timeoutRunnable = TimeoutRunnable() private var isTransitionRunning = false private var isAnimatedCancelRunning = false Loading Loading @@ -92,7 +89,7 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( cancelTransition(endValue = 1f, animate = true) } } FOLD_UPDATE_FINISH_FULL_OPEN -> { FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_ABORTED -> { // Do not cancel if we haven't started the transition yet. // This could happen when we fully unfolded the device before the screen // became available. In this case we start and immediately cancel the animation Loading @@ -106,9 +103,13 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( cancelTransition(endValue = 0f, animate = false) } FOLD_UPDATE_START_CLOSING -> { // The transition might be already running as the device might start closing several // times before reaching an end state. if (!isTransitionRunning) { startTransition(startValue = 1f) } } } if (DEBUG) { Log.d(TAG, "onFoldUpdate = $update") Loading @@ -116,8 +117,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( } private fun cancelTransition(endValue: Float, animate: Boolean) { handler.removeCallbacks(timeoutRunnable) if (isTransitionRunning && animate) { isAnimatedCancelRunning = true springAnimation.animateToFinalPosition(endValue) Loading Loading @@ -175,8 +174,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( } springAnimation.start() handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS) } override fun addCallback(listener: TransitionProgressListener) { Loading @@ -187,13 +184,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( listeners.remove(listener) } private inner class TimeoutRunnable : Runnable { override fun run() { cancelTransition(endValue = 1f, animate = true) } } private object AnimationProgressProperty : FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") { Loading @@ -212,7 +202,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" private const val DEBUG = true private const val TRANSITION_TIMEOUT_MILLIS = 2000L private const val SPRING_STIFFNESS = 200.0f private const val MINIMAL_VISIBLE_CHANGE = 0.001f private const val FINAL_HINGE_ANGLE_POSITION = 165f packages/SystemUI/shared/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +90 −26 Original line number Diff line number Diff line Loading @@ -15,14 +15,19 @@ */ package com.android.systemui.unfold.updates import android.annotation.FloatRange import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.Handler import android.util.Log import androidx.annotation.VisibleForTesting import androidx.core.util.Consumer import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES import com.android.systemui.unfold.updates.hinge.HingeAngleProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import java.util.concurrent.Executor class DeviceFoldStateProvider( Loading @@ -30,7 +35,8 @@ class DeviceFoldStateProvider( private val hingeAngleProvider: HingeAngleProvider, private val screenStatusProvider: ScreenStatusProvider, private val deviceStateManager: DeviceStateManager, private val mainExecutor: Executor private val mainExecutor: Executor, private val handler: Handler ) : FoldStateProvider { private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf() Loading @@ -38,9 +44,13 @@ class DeviceFoldStateProvider( @FoldUpdate private var lastFoldUpdate: Int? = null @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f private val hingeAngleListener = HingeAngleListener() private val screenListener = ScreenStatusListener() private val foldStateListener = FoldStateListener(context) private val timeoutRunnable = TimeoutRunnable() private var isFolded = false private var isUnfoldHandled = true Loading Loading @@ -72,47 +82,69 @@ class DeviceFoldStateProvider( override val isFullyOpened: Boolean get() = !isFolded && lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN private val isTransitionInProgess: Boolean get() = lastFoldUpdate == FOLD_UPDATE_START_OPENING || lastFoldUpdate == FOLD_UPDATE_START_CLOSING private fun onHingeAngle(angle: Float) { when (lastFoldUpdate) { FOLD_UPDATE_FINISH_FULL_OPEN -> { if (FULLY_OPEN_DEGREES - angle > START_CLOSING_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_START_CLOSING outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_CLOSING) } } } FOLD_UPDATE_START_OPENING -> { if (FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) } } } FOLD_UPDATE_START_CLOSING -> { if (FULLY_OPEN_DEGREES - angle < START_CLOSING_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) } if (DEBUG) { Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle") } val isClosing = angle < lastHingeAngle val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING if (isClosing && !closingEventDispatched && !isFullyOpened) { notifyFoldUpdate(FOLD_UPDATE_START_CLOSING) } if (isTransitionInProgess) { if (isFullyOpened) { notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) cancelTimeout() } else { // The timeout will trigger some constant time after the last angle update. rescheduleAbortAnimationTimeout() } } lastHingeAngle = angle outputListeners.forEach { it.onHingeAngleUpdate(angle) } } private inner class FoldStateListener(context: Context) : DeviceStateManager.FoldStateListener(context, { folded: Boolean -> isFolded = folded lastHingeAngle = FULLY_CLOSED_DEGREES if (folded) { lastFoldUpdate = FOLD_UPDATE_FINISH_CLOSED outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) } hingeAngleProvider.stop() notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) cancelTimeout() isUnfoldHandled = false } else { lastFoldUpdate = FOLD_UPDATE_START_OPENING outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_OPENING) } notifyFoldUpdate(FOLD_UPDATE_START_OPENING) rescheduleAbortAnimationTimeout() hingeAngleProvider.start() } }) private fun notifyFoldUpdate(@FoldUpdate update: Int) { if (DEBUG) { Log.d(TAG, stateToString(update)) } outputListeners.forEach { it.onFoldUpdate(update) } lastFoldUpdate = update } private fun rescheduleAbortAnimationTimeout() { if (isTransitionInProgess) { cancelTimeout() } handler.postDelayed(timeoutRunnable, ABORT_CLOSING_MILLIS) } private fun cancelTimeout() { handler.removeCallbacks(timeoutRunnable) } private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener { Loading @@ -136,7 +168,39 @@ class DeviceFoldStateProvider( onHingeAngle(angle) } } private inner class TimeoutRunnable : Runnable { override fun run() { notifyFoldUpdate(FOLD_UPDATE_ABORTED) } } } private fun stateToString(@FoldUpdate update: Int): String { return when (update) { FOLD_UPDATE_START_OPENING -> "START_OPENING" FOLD_UPDATE_HALF_OPEN -> "HALF_OPEN" FOLD_UPDATE_START_CLOSING -> "START_CLOSING" FOLD_UPDATE_ABORTED -> "ABORTED" FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE" FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN" FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN" FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED" else -> "UNKNOWN" } } private const val START_CLOSING_THRESHOLD_DEGREES = 95f private const val FULLY_OPEN_THRESHOLD_DEGREES = 15f No newline at end of file private const val TAG = "DeviceFoldProvider" private const val DEBUG = false /** * Time after which [FOLD_UPDATE_ABORTED] is emitted following a [FOLD_UPDATE_START_CLOSING] or * [FOLD_UPDATE_START_OPENING] event, if an end state is not reached. */ @VisibleForTesting const val ABORT_CLOSING_MILLIS = 1000L /** Threshold after which we consider the device fully unfolded. */ @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f No newline at end of file packages/SystemUI/shared/src/com/android/systemui/unfold/updates/FoldStateProvider.kt +6 −4 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ interface FoldStateProvider : CallbackController<FoldUpdatesListener> { FOLD_UPDATE_START_OPENING, FOLD_UPDATE_HALF_OPEN, FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED, FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_FINISH_HALF_OPEN, FOLD_UPDATE_FINISH_FULL_OPEN, Loading @@ -51,7 +52,8 @@ interface FoldStateProvider : CallbackController<FoldUpdatesListener> { const val FOLD_UPDATE_START_OPENING = 0 const val FOLD_UPDATE_HALF_OPEN = 1 const val FOLD_UPDATE_START_CLOSING = 2 const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 3 const val FOLD_UPDATE_FINISH_HALF_OPEN = 4 const val FOLD_UPDATE_FINISH_FULL_OPEN = 5 const val FOLD_UPDATE_FINISH_CLOSED = 6 const val FOLD_UPDATE_ABORTED = 3 const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 4 const val FOLD_UPDATE_FINISH_HALF_OPEN = 5 const val FOLD_UPDATE_FINISH_FULL_OPEN = 6 const val FOLD_UPDATE_FINISH_CLOSED = 7 packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +122 −4 Original line number Diff line number Diff line Loading @@ -18,7 +18,9 @@ package com.android.systemui.unfold.updates import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.DeviceStateManager.FoldStateListener import android.os.Handler import android.testing.AndroidTestingRunner import androidx.core.util.Consumer import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.updates.hinge.HingeAngleProvider Loading @@ -31,9 +33,12 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import java.lang.Exception @RunWith(AndroidTestingRunner::class) @SmallTest Loading @@ -48,16 +53,28 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { @Mock private lateinit var deviceStateManager: DeviceStateManager private lateinit var foldStateProvider: FoldStateProvider @Mock private lateinit var handler: Handler @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener> @Captor private lateinit var screenOnListenerCaptor: ArgumentCaptor<ScreenListener> @Captor private lateinit var hingeAngleCaptor: ArgumentCaptor<Consumer<Float>> private lateinit var foldStateProvider: DeviceFoldStateProvider private val foldUpdates: MutableList<Int> = arrayListOf() private val hingeAngleUpdates: MutableList<Float> = arrayListOf() private val foldStateListenerCaptor = ArgumentCaptor.forClass(FoldStateListener::class.java) private var foldedDeviceState: Int = 0 private var unfoldedDeviceState: Int = 0 private val screenOnListenerCaptor = ArgumentCaptor.forClass(ScreenListener::class.java) private var scheduledRunnable: Runnable? = null private var scheduledRunnableDelay: Long? = null @Before fun setUp() { Loading @@ -75,7 +92,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { hingeAngleProvider, screenStatusProvider, deviceStateManager, context.mainExecutor context.mainExecutor, handler ) foldStateProvider.addCallback(object : FoldStateProvider.FoldUpdatesListener { Loading @@ -91,6 +109,22 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture()) verify(screenStatusProvider).addCallback(screenOnListenerCaptor.capture()) verify(hingeAngleProvider).addCallback(hingeAngleCaptor.capture()) whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock -> scheduledRunnable = invocationOnMock.getArgument<Runnable>(0) scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1) null } whenever(handler.removeCallbacks(any<Runnable>())).then { invocationOnMock -> val removedRunnable = invocationOnMock.getArgument<Runnable>(0) if (removedRunnable == scheduledRunnable) { scheduledRunnableDelay = null scheduledRunnable = null } null } } @Test Loading Loading @@ -167,6 +201,86 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { assertThat(foldUpdates).isEmpty() } @Test fun startClosingEvent_afterTimeout_abortEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED) } @Test fun startClosingEvent_beforeTimeout_abortNotEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS - 1) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_eventBeforeTimeout_oneEventEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) simulateTimeout(ABORT_CLOSING_MILLIS - 1) sendHingeAngleEvent(80) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) simulateTimeout(ABORT_CLOSING_MILLIS - 1) // The timeout should not trigger here. sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS) // The timeout should trigger here. assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED) } @Test fun startClosingEvent_shortTimeBetween_emitsOnlyOneEvents() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) sendHingeAngleEvent(80) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_whileClosing_emittedDespiteInitialAngle() { val maxAngle = 180 - FULLY_OPEN_THRESHOLD_DEGREES.toInt() for (i in 1..maxAngle) { foldUpdates.clear() simulateFolding(startAngle = i) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) simulateTimeout() // Timeout to set the state to aborted. } } private fun simulateTimeout(waitTime: Long = ABORT_CLOSING_MILLIS) { val runnableDelay = scheduledRunnableDelay ?: throw Exception("No runnable scheduled.") if (waitTime >= runnableDelay) { scheduledRunnable?.run() scheduledRunnable = null scheduledRunnableDelay = null } } private fun simulateFolding(startAngle: Int) { sendHingeAngleEvent(startAngle) sendHingeAngleEvent(startAngle - 1) } private fun setFoldState(folded: Boolean) { val state = if (folded) foldedDeviceState else unfoldedDeviceState foldStateListenerCaptor.value.onStateChanged(state) Loading @@ -175,4 +289,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { private fun fireScreenOnEvent() { screenOnListenerCaptor.value.onScreenTurnedOn() } private fun sendHingeAngleEvent(angle: Int) { hingeAngleCaptor.value.accept(angle.toFloat()) } } Loading
packages/SystemUI/shared/src/com/android/systemui/unfold/UnfoldTransitionFactory.kt +3 −5 Original line number Diff line number Diff line Loading @@ -60,14 +60,12 @@ fun createUnfoldTransitionProgressProvider( hingeAngleProvider, screenStatusProvider, deviceStateManager, mainExecutor mainExecutor, mainHandler ) val unfoldTransitionProgressProvider = if (config.isHingeAngleEnabled) { PhysicsBasedUnfoldTransitionProgressProvider( mainHandler, foldStateProvider ) PhysicsBasedUnfoldTransitionProgressProvider(foldStateProvider) } else { FixedTimingTransitionProgressProvider(foldStateProvider) } Loading
packages/SystemUI/shared/src/com/android/systemui/unfold/progress/PhysicsBasedUnfoldTransitionProgressProvider.kt +8 −19 Original line number Diff line number Diff line Loading @@ -15,7 +15,6 @@ */ package com.android.systemui.unfold.progress import android.os.Handler import android.util.Log import android.util.MathUtils.saturate import androidx.dynamicanimation.animation.DynamicAnimation Loading @@ -24,9 +23,10 @@ import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.systemui.unfold.UnfoldTransitionProgressProvider import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.updates.FOLD_UPDATE_ABORTED import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_CLOSED import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_FULL_OPEN import com.android.systemui.unfold.updates.FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE import com.android.systemui.unfold.updates.FoldStateProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate Loading @@ -39,7 +39,6 @@ import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener * - doesn't handle postures */ internal class PhysicsBasedUnfoldTransitionProgressProvider( private val handler: Handler, private val foldStateProvider: FoldStateProvider ) : UnfoldTransitionProgressProvider, Loading @@ -51,8 +50,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider) } private val timeoutRunnable = TimeoutRunnable() private var isTransitionRunning = false private var isAnimatedCancelRunning = false Loading Loading @@ -92,7 +89,7 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( cancelTransition(endValue = 1f, animate = true) } } FOLD_UPDATE_FINISH_FULL_OPEN -> { FOLD_UPDATE_FINISH_FULL_OPEN, FOLD_UPDATE_ABORTED -> { // Do not cancel if we haven't started the transition yet. // This could happen when we fully unfolded the device before the screen // became available. In this case we start and immediately cancel the animation Loading @@ -106,9 +103,13 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( cancelTransition(endValue = 0f, animate = false) } FOLD_UPDATE_START_CLOSING -> { // The transition might be already running as the device might start closing several // times before reaching an end state. if (!isTransitionRunning) { startTransition(startValue = 1f) } } } if (DEBUG) { Log.d(TAG, "onFoldUpdate = $update") Loading @@ -116,8 +117,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( } private fun cancelTransition(endValue: Float, animate: Boolean) { handler.removeCallbacks(timeoutRunnable) if (isTransitionRunning && animate) { isAnimatedCancelRunning = true springAnimation.animateToFinalPosition(endValue) Loading Loading @@ -175,8 +174,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( } springAnimation.start() handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS) } override fun addCallback(listener: TransitionProgressListener) { Loading @@ -187,13 +184,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( listeners.remove(listener) } private inner class TimeoutRunnable : Runnable { override fun run() { cancelTransition(endValue = 1f, animate = true) } } private object AnimationProgressProperty : FloatPropertyCompat<PhysicsBasedUnfoldTransitionProgressProvider>("animation_progress") { Loading @@ -212,7 +202,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider( private const val TAG = "PhysicsBasedUnfoldTransitionProgressProvider" private const val DEBUG = true private const val TRANSITION_TIMEOUT_MILLIS = 2000L private const val SPRING_STIFFNESS = 200.0f private const val MINIMAL_VISIBLE_CHANGE = 0.001f private const val FINAL_HINGE_ANGLE_POSITION = 165f
packages/SystemUI/shared/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt +90 −26 Original line number Diff line number Diff line Loading @@ -15,14 +15,19 @@ */ package com.android.systemui.unfold.updates import android.annotation.FloatRange import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.Handler import android.util.Log import androidx.annotation.VisibleForTesting import androidx.core.util.Consumer import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdate import com.android.systemui.unfold.updates.FoldStateProvider.FoldUpdatesListener import com.android.systemui.unfold.updates.hinge.FULLY_CLOSED_DEGREES import com.android.systemui.unfold.updates.hinge.FULLY_OPEN_DEGREES import com.android.systemui.unfold.updates.hinge.HingeAngleProvider import com.android.systemui.unfold.updates.screen.ScreenStatusProvider import java.util.concurrent.Executor class DeviceFoldStateProvider( Loading @@ -30,7 +35,8 @@ class DeviceFoldStateProvider( private val hingeAngleProvider: HingeAngleProvider, private val screenStatusProvider: ScreenStatusProvider, private val deviceStateManager: DeviceStateManager, private val mainExecutor: Executor private val mainExecutor: Executor, private val handler: Handler ) : FoldStateProvider { private val outputListeners: MutableList<FoldUpdatesListener> = mutableListOf() Loading @@ -38,9 +44,13 @@ class DeviceFoldStateProvider( @FoldUpdate private var lastFoldUpdate: Int? = null @FloatRange(from = 0.0, to = 180.0) private var lastHingeAngle: Float = 0f private val hingeAngleListener = HingeAngleListener() private val screenListener = ScreenStatusListener() private val foldStateListener = FoldStateListener(context) private val timeoutRunnable = TimeoutRunnable() private var isFolded = false private var isUnfoldHandled = true Loading Loading @@ -72,47 +82,69 @@ class DeviceFoldStateProvider( override val isFullyOpened: Boolean get() = !isFolded && lastFoldUpdate == FOLD_UPDATE_FINISH_FULL_OPEN private val isTransitionInProgess: Boolean get() = lastFoldUpdate == FOLD_UPDATE_START_OPENING || lastFoldUpdate == FOLD_UPDATE_START_CLOSING private fun onHingeAngle(angle: Float) { when (lastFoldUpdate) { FOLD_UPDATE_FINISH_FULL_OPEN -> { if (FULLY_OPEN_DEGREES - angle > START_CLOSING_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_START_CLOSING outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_CLOSING) } } } FOLD_UPDATE_START_OPENING -> { if (FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) } } } FOLD_UPDATE_START_CLOSING -> { if (FULLY_OPEN_DEGREES - angle < START_CLOSING_THRESHOLD_DEGREES) { lastFoldUpdate = FOLD_UPDATE_FINISH_FULL_OPEN outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) } if (DEBUG) { Log.d(TAG, "Hinge angle: $angle, lastHingeAngle: $lastHingeAngle") } val isClosing = angle < lastHingeAngle val isFullyOpened = FULLY_OPEN_DEGREES - angle < FULLY_OPEN_THRESHOLD_DEGREES val closingEventDispatched = lastFoldUpdate == FOLD_UPDATE_START_CLOSING if (isClosing && !closingEventDispatched && !isFullyOpened) { notifyFoldUpdate(FOLD_UPDATE_START_CLOSING) } if (isTransitionInProgess) { if (isFullyOpened) { notifyFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) cancelTimeout() } else { // The timeout will trigger some constant time after the last angle update. rescheduleAbortAnimationTimeout() } } lastHingeAngle = angle outputListeners.forEach { it.onHingeAngleUpdate(angle) } } private inner class FoldStateListener(context: Context) : DeviceStateManager.FoldStateListener(context, { folded: Boolean -> isFolded = folded lastHingeAngle = FULLY_CLOSED_DEGREES if (folded) { lastFoldUpdate = FOLD_UPDATE_FINISH_CLOSED outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) } hingeAngleProvider.stop() notifyFoldUpdate(FOLD_UPDATE_FINISH_CLOSED) cancelTimeout() isUnfoldHandled = false } else { lastFoldUpdate = FOLD_UPDATE_START_OPENING outputListeners.forEach { it.onFoldUpdate(FOLD_UPDATE_START_OPENING) } notifyFoldUpdate(FOLD_UPDATE_START_OPENING) rescheduleAbortAnimationTimeout() hingeAngleProvider.start() } }) private fun notifyFoldUpdate(@FoldUpdate update: Int) { if (DEBUG) { Log.d(TAG, stateToString(update)) } outputListeners.forEach { it.onFoldUpdate(update) } lastFoldUpdate = update } private fun rescheduleAbortAnimationTimeout() { if (isTransitionInProgess) { cancelTimeout() } handler.postDelayed(timeoutRunnable, ABORT_CLOSING_MILLIS) } private fun cancelTimeout() { handler.removeCallbacks(timeoutRunnable) } private inner class ScreenStatusListener : ScreenStatusProvider.ScreenListener { Loading @@ -136,7 +168,39 @@ class DeviceFoldStateProvider( onHingeAngle(angle) } } private inner class TimeoutRunnable : Runnable { override fun run() { notifyFoldUpdate(FOLD_UPDATE_ABORTED) } } } private fun stateToString(@FoldUpdate update: Int): String { return when (update) { FOLD_UPDATE_START_OPENING -> "START_OPENING" FOLD_UPDATE_HALF_OPEN -> "HALF_OPEN" FOLD_UPDATE_START_CLOSING -> "START_CLOSING" FOLD_UPDATE_ABORTED -> "ABORTED" FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE -> "UNFOLDED_SCREEN_AVAILABLE" FOLD_UPDATE_FINISH_HALF_OPEN -> "FINISH_HALF_OPEN" FOLD_UPDATE_FINISH_FULL_OPEN -> "FINISH_FULL_OPEN" FOLD_UPDATE_FINISH_CLOSED -> "FINISH_CLOSED" else -> "UNKNOWN" } } private const val START_CLOSING_THRESHOLD_DEGREES = 95f private const val FULLY_OPEN_THRESHOLD_DEGREES = 15f No newline at end of file private const val TAG = "DeviceFoldProvider" private const val DEBUG = false /** * Time after which [FOLD_UPDATE_ABORTED] is emitted following a [FOLD_UPDATE_START_CLOSING] or * [FOLD_UPDATE_START_OPENING] event, if an end state is not reached. */ @VisibleForTesting const val ABORT_CLOSING_MILLIS = 1000L /** Threshold after which we consider the device fully unfolded. */ @VisibleForTesting const val FULLY_OPEN_THRESHOLD_DEGREES = 15f No newline at end of file
packages/SystemUI/shared/src/com/android/systemui/unfold/updates/FoldStateProvider.kt +6 −4 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ interface FoldStateProvider : CallbackController<FoldUpdatesListener> { FOLD_UPDATE_START_OPENING, FOLD_UPDATE_HALF_OPEN, FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED, FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE, FOLD_UPDATE_FINISH_HALF_OPEN, FOLD_UPDATE_FINISH_FULL_OPEN, Loading @@ -51,7 +52,8 @@ interface FoldStateProvider : CallbackController<FoldUpdatesListener> { const val FOLD_UPDATE_START_OPENING = 0 const val FOLD_UPDATE_HALF_OPEN = 1 const val FOLD_UPDATE_START_CLOSING = 2 const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 3 const val FOLD_UPDATE_FINISH_HALF_OPEN = 4 const val FOLD_UPDATE_FINISH_FULL_OPEN = 5 const val FOLD_UPDATE_FINISH_CLOSED = 6 const val FOLD_UPDATE_ABORTED = 3 const val FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE = 4 const val FOLD_UPDATE_FINISH_HALF_OPEN = 5 const val FOLD_UPDATE_FINISH_FULL_OPEN = 6 const val FOLD_UPDATE_FINISH_CLOSED = 7
packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt +122 −4 Original line number Diff line number Diff line Loading @@ -18,7 +18,9 @@ package com.android.systemui.unfold.updates import android.hardware.devicestate.DeviceStateManager import android.hardware.devicestate.DeviceStateManager.FoldStateListener import android.os.Handler import android.testing.AndroidTestingRunner import androidx.core.util.Consumer import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.updates.hinge.HingeAngleProvider Loading @@ -31,9 +33,12 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations import java.lang.Exception @RunWith(AndroidTestingRunner::class) @SmallTest Loading @@ -48,16 +53,28 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { @Mock private lateinit var deviceStateManager: DeviceStateManager private lateinit var foldStateProvider: FoldStateProvider @Mock private lateinit var handler: Handler @Captor private lateinit var foldStateListenerCaptor: ArgumentCaptor<FoldStateListener> @Captor private lateinit var screenOnListenerCaptor: ArgumentCaptor<ScreenListener> @Captor private lateinit var hingeAngleCaptor: ArgumentCaptor<Consumer<Float>> private lateinit var foldStateProvider: DeviceFoldStateProvider private val foldUpdates: MutableList<Int> = arrayListOf() private val hingeAngleUpdates: MutableList<Float> = arrayListOf() private val foldStateListenerCaptor = ArgumentCaptor.forClass(FoldStateListener::class.java) private var foldedDeviceState: Int = 0 private var unfoldedDeviceState: Int = 0 private val screenOnListenerCaptor = ArgumentCaptor.forClass(ScreenListener::class.java) private var scheduledRunnable: Runnable? = null private var scheduledRunnableDelay: Long? = null @Before fun setUp() { Loading @@ -75,7 +92,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { hingeAngleProvider, screenStatusProvider, deviceStateManager, context.mainExecutor context.mainExecutor, handler ) foldStateProvider.addCallback(object : FoldStateProvider.FoldUpdatesListener { Loading @@ -91,6 +109,22 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture()) verify(screenStatusProvider).addCallback(screenOnListenerCaptor.capture()) verify(hingeAngleProvider).addCallback(hingeAngleCaptor.capture()) whenever(handler.postDelayed(any<Runnable>(), any())).then { invocationOnMock -> scheduledRunnable = invocationOnMock.getArgument<Runnable>(0) scheduledRunnableDelay = invocationOnMock.getArgument<Long>(1) null } whenever(handler.removeCallbacks(any<Runnable>())).then { invocationOnMock -> val removedRunnable = invocationOnMock.getArgument<Runnable>(0) if (removedRunnable == scheduledRunnable) { scheduledRunnableDelay = null scheduledRunnable = null } null } } @Test Loading Loading @@ -167,6 +201,86 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { assertThat(foldUpdates).isEmpty() } @Test fun startClosingEvent_afterTimeout_abortEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED) } @Test fun startClosingEvent_beforeTimeout_abortNotEmitted() { sendHingeAngleEvent(90) sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS - 1) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_eventBeforeTimeout_oneEventEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) simulateTimeout(ABORT_CLOSING_MILLIS - 1) sendHingeAngleEvent(80) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_timeoutAfterTimeoutRescheduled_abortEmitted() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) simulateTimeout(ABORT_CLOSING_MILLIS - 1) // The timeout should not trigger here. sendHingeAngleEvent(80) simulateTimeout(ABORT_CLOSING_MILLIS) // The timeout should trigger here. assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING, FOLD_UPDATE_ABORTED) } @Test fun startClosingEvent_shortTimeBetween_emitsOnlyOneEvents() { sendHingeAngleEvent(180) sendHingeAngleEvent(90) sendHingeAngleEvent(80) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) } @Test fun startClosingEvent_whileClosing_emittedDespiteInitialAngle() { val maxAngle = 180 - FULLY_OPEN_THRESHOLD_DEGREES.toInt() for (i in 1..maxAngle) { foldUpdates.clear() simulateFolding(startAngle = i) assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING) simulateTimeout() // Timeout to set the state to aborted. } } private fun simulateTimeout(waitTime: Long = ABORT_CLOSING_MILLIS) { val runnableDelay = scheduledRunnableDelay ?: throw Exception("No runnable scheduled.") if (waitTime >= runnableDelay) { scheduledRunnable?.run() scheduledRunnable = null scheduledRunnableDelay = null } } private fun simulateFolding(startAngle: Int) { sendHingeAngleEvent(startAngle) sendHingeAngleEvent(startAngle - 1) } private fun setFoldState(folded: Boolean) { val state = if (folded) foldedDeviceState else unfoldedDeviceState foldStateListenerCaptor.value.onStateChanged(state) Loading @@ -175,4 +289,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() { private fun fireScreenOnEvent() { screenOnListenerCaptor.value.onScreenTurnedOn() } private fun sendHingeAngleEvent(angle: Int) { hingeAngleCaptor.value.accept(angle.toFloat()) } }