Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9a33c9de authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Improve fold state provider logic handling posture" into sc-v2-dev

parents 2176d951 5aef2c1f
Loading
Loading
Loading
Loading
+3 −5
Original line number Diff line number Diff line
@@ -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)
    }
+8 −19
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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,
@@ -51,8 +50,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider(
            addEndListener(this@PhysicsBasedUnfoldTransitionProgressProvider)
        }

    private val timeoutRunnable = TimeoutRunnable()

    private var isTransitionRunning = false
    private var isAnimatedCancelRunning = false

@@ -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
@@ -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")
@@ -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)
@@ -175,8 +174,6 @@ internal class PhysicsBasedUnfoldTransitionProgressProvider(
        }

        springAnimation.start()

        handler.postDelayed(timeoutRunnable, TRANSITION_TIMEOUT_MILLIS)
    }

    override fun addCallback(listener: TransitionProgressListener) {
@@ -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") {

@@ -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
+90 −26
Original line number Diff line number Diff line
@@ -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(
@@ -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()
@@ -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
@@ -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 {

@@ -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
+6 −4
Original line number Diff line number Diff line
@@ -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,
@@ -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
+122 −4
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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() {
@@ -75,7 +92,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
            hingeAngleProvider,
            screenStatusProvider,
            deviceStateManager,
            context.mainExecutor
            context.mainExecutor,
            handler
        )

        foldStateProvider.addCallback(object : FoldStateProvider.FoldUpdatesListener {
@@ -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
@@ -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)
@@ -175,4 +289,8 @@ class DeviceFoldStateProviderTest : SysuiTestCase() {
    private fun fireScreenOnEvent() {
        screenOnListenerCaptor.value.onScreenTurnedOn()
    }

    private fun sendHingeAngleEvent(angle: Int) {
        hingeAngleCaptor.value.accept(angle.toFloat())
    }
}