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

Commit 48a34c93 authored by Mike Schneider's avatar Mike Schneider
Browse files

Refactor MotionValueDebugger to use CompositionLocal

Bug: 391553479
Test: Unit tests
Flag: EXEMPT No production use / debug code only
Change-Id: Ia02be137323094982d342849fddc3ee4869743d9
parent 32be6d70
Loading
Loading
Loading
Loading
+12 −10
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.mechanics.compose.modifier

import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -30,10 +29,12 @@ import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.LayoutModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.unit.Constraints
@@ -43,7 +44,7 @@ import com.android.mechanics.GestureContext
import com.android.mechanics.MotionValue
import com.android.mechanics.MotionValue.Companion.StableThresholdEffect
import com.android.mechanics.compose.modifier.MotionDriver.RequestConstraints
import com.android.mechanics.debug.findMotionValueDebugger
import com.android.mechanics.debug.LocalMotionValueDebugController
import com.android.mechanics.spec.MotionSpec
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
@@ -188,7 +189,11 @@ private data class MotionDriverElement(val gestureContext: GestureContext, val l
}

private class MotionDriverNode(override var gestureContext: GestureContext) :
    Modifier.Node(), TraversableNode, LayoutModifierNode, MotionDriver {
    Modifier.Node(),
    TraversableNode,
    LayoutModifierNode,
    MotionDriver,
    CompositionLocalConsumerModifierNode {
    private val animatedValues = mutableListOf<AnimatedApproachMeasurementImpl>()
    private var driverCoordinates: LayoutCoordinates? = null
    private var lookAheadHeight: Int = 0
@@ -251,13 +256,10 @@ private class MotionDriverNode(override var gestureContext: GestureContext) :
            )
        animatedValues += animatedApproachMeasurement

        val debugController = if (debug) currentValueOf(LocalMotionValueDebugController) else null
        coroutineScope.launch {
            val disposableHandle =
                if (debug) {
                    findMotionValueDebugger()?.register(animatedApproachMeasurement.motionValue)
                } else {
                    null
                }
                debugController?.register(animatedApproachMeasurement.motionValue)
            try {
                animatedApproachMeasurement.keepRunningWhileObserved()
            } finally {
@@ -266,7 +268,7 @@ private class MotionDriverNode(override var gestureContext: GestureContext) :
        }

        coroutineScope.launch {
            while (true) {
            while (animatedApproachMeasurement.isObserved) {
                withFrameNanos { animatedApproachMeasurement.computeOutput() }
            }
        }
@@ -282,7 +284,7 @@ private class MotionDriverNode(override var gestureContext: GestureContext) :
        stableThreshold: Float,
        private val onDispose: AnimatedApproachMeasurementImpl.() -> Unit,
    ) : MotionDriver.AnimatedApproachMeasurement {
        private var isObserved = true
        var isObserved = true
        private var lastInput: Float? = null

        val motionValue: MotionValue =
+56 −51
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
@@ -52,8 +53,8 @@ import com.android.compose.animation.scene.featureOfElement
import com.android.compose.animation.scene.mechanics.rememberGestureContext
import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutState
import com.android.compose.animation.scene.transitions
import com.android.mechanics.debug.MotionValueDebuggerState
import com.android.mechanics.debug.motionValueDebugger
import com.android.mechanics.debug.LocalMotionValueDebugController
import com.android.mechanics.debug.MotionValueDebugController
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.rememberMotionBuilderContext
import com.android.mechanics.testing.FakeMotionSpecBuilderContext
@@ -84,7 +85,7 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
            createGoldenPathManager("frameworks/libs/systemui/mechanics/compose/tests/goldens")
        )

    private val debugger = MotionValueDebuggerState()
    private val debugger = MotionValueDebugController()

    private fun assertVerticalTactileSurfaceRevealMotion(
        goldenName: String,
@@ -135,6 +136,9 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
            val motion =
                recordMotion(
                    content = {
                        CompositionLocalProvider(
                            LocalMotionValueDebugController provides debugger
                        ) {
                            state =
                                rememberMutableSceneTransitionLayoutState(
                                    initialScene = gestureControl.startScene,
@@ -154,8 +158,7 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
                                modifier =
                                    Modifier.background(Color.Yellow)
                                        .size(ContainerSize)
                                    .testTag(STL_TAG)
                                    .motionValueDebugger(debugger),
                                        .testTag(STL_TAG),
                                implicitTestTags = true,
                            ) {
                                scene(
@@ -178,7 +181,9 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
                                                Swipe.Up to
                                                    UserActionResult.HideOverlay(ExpandedOverlay)
                                            ),
                                    content = { TestContent(Modifier.border(2.dp, Color.Magenta)) },
                                        content = {
                                            TestContent(Modifier.border(2.dp, Color.Magenta))
                                        },
                                    )
                                } else {
                                    scene(
@@ -188,6 +193,7 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
                                    )
                                }
                            }
                        }
                    },
                    ComposeRecordingSpec(
                        recording = {
@@ -197,8 +203,7 @@ class VerticalTactileSurfaceRevealModifierTest(val useOverlays: Boolean) :
                            )

                            awaitCondition {
                                !state.isTransitioning() &&
                                    debugger.observedMotionValues.all { it.isStable }
                                !state.isTransitioning() && debugger.observed.all { it.isStable }
                            }
                        },
                        timeSeriesCapture = {
+67 −65
Original line number Diff line number Diff line
@@ -16,93 +16,87 @@

package com.android.mechanics.debug

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
import androidx.compose.ui.node.DelegatableNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.TraversableNode
import androidx.compose.ui.node.findNearestAncestor
import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.InspectorInfo
import com.android.mechanics.MotionValue
import com.android.mechanics.debug.MotionValueDebuggerNode.Companion.TRAVERSAL_NODE_KEY
import kotlinx.coroutines.DisposableHandle

/** State for the [MotionValueDebugger]. */
sealed interface MotionValueDebuggerState {
    val observedMotionValues: List<MotionValue>
/** Keeps track of MotionValues that are registered for debug-inspection. */
class MotionValueDebugController {
    private val observedMotionValues = mutableStateListOf<MotionValue>()

    /**
     * Registers a [MotionValue] to be debugged.
     *
     * Clients must call [DisposableHandle.dispose] when done.
     */
    fun register(motionValue: MotionValue): DisposableHandle {
        observedMotionValues.add(motionValue)
        return DisposableHandle { observedMotionValues.remove(motionValue) }
    }

/** Factory for [MotionValueDebugger]. */
fun MotionValueDebuggerState(): MotionValueDebuggerState {
    return MotionValueDebuggerStateImpl()
    /** The currently registered `MotionValues`. */
    val observed: List<MotionValue>
        get() = observedMotionValues
}

/** Collector for [MotionValue]s in the Node subtree that should be observed for debug purposes. */
fun Modifier.motionValueDebugger(state: MotionValueDebuggerState): Modifier =
    this.then(MotionValueDebuggerElement(state as MotionValueDebuggerStateImpl))
/** Composition-local to provide a [MotionValueDebugController]. */
val LocalMotionValueDebugController = staticCompositionLocalOf<MotionValueDebugController?> { null }

/**
 * [motionValueDebugger]'s interface, nodes in the subtree of a [motionValueDebugger] can retrieve
 * it using [findMotionValueDebugger].
 * Provides a [MotionValueDebugController], to which [MotionValue]s within [content] can be
 * registered to.
 *
 * With [enableDebugger] set to `false` (or this composable not being in the composition in the
 * first place), downstream [debugMotionValue] and [DebugEffect] will be no-ops.
 */
sealed interface MotionValueDebugger {
    fun register(motionValue: MotionValue): DisposableHandle
@Composable
fun MotionValueDebuggerProvider(enableDebugger: Boolean = true, content: @Composable () -> Unit) {
    val debugger =
        remember(enableDebugger) { if (enableDebugger) MotionValueDebugController() else null }
    CompositionLocalProvider(LocalMotionValueDebugController provides debugger) { content() }
}

/** Finds a [MotionValueDebugger] that was registered via a [motionValueDebugger] modifier. */
fun DelegatableNode.findMotionValueDebugger(): MotionValueDebugger? {
    return findNearestAncestor(TRAVERSAL_NODE_KEY) as? MotionValueDebugger
}

/** Registers the motion value for debugging with the parent [MotionValue]. */
/** Registers the [motionValue] with the [LocalMotionValueDebugController], if available. */
fun Modifier.debugMotionValue(motionValue: MotionValue): Modifier =
    this.then(DebugMotionValueElement(motionValue))

internal class MotionValueDebuggerNode(internal var state: MotionValueDebuggerStateImpl) :
    Modifier.Node(), TraversableNode, MotionValueDebugger {

    override val traverseKey = TRAVERSAL_NODE_KEY

    override fun register(motionValue: MotionValue): DisposableHandle {
        val state = state
        state.observedMotionValues.add(motionValue)
        return DisposableHandle { state.observedMotionValues.remove(motionValue) }
    }

    companion object {
        const val TRAVERSAL_NODE_KEY = "com.android.mechanics.debug.DEBUG_CONNECTOR_NODE_KEY"
    }
}

private data class MotionValueDebuggerElement(val state: MotionValueDebuggerStateImpl) :
    ModifierNodeElement<MotionValueDebuggerNode>() {
    override fun create(): MotionValueDebuggerNode = MotionValueDebuggerNode(state)

    override fun InspectorInfo.inspectableProperties() {
        // Intentionally empty
/** Registers the [motionValue] with the [LocalMotionValueDebugController], if available. */
@Composable
fun DebugEffect(motionValue: MotionValue) {
    val debugger = LocalMotionValueDebugController.current
    if (debugger != null) {
        DisposableEffect(debugger, motionValue) {
            val handle = debugger.register(motionValue)
            onDispose { handle.dispose() }
        }

    override fun update(node: MotionValueDebuggerNode) {
        check(node.state === state)
    }
}

internal class DebugMotionValueNode(motionValue: MotionValue) : Modifier.Node() {

    private var debugger: MotionValueDebugger? = null

    internal var motionValue = motionValue
        set(value) {
            registration?.dispose()
            registration = debugger?.register(value)
            field = value
        }
/**
 * [DelegatableNode] to register the [motionValue] with the [LocalMotionValueDebugController], if
 * available.
 */
class DebugMotionValueNode(motionValue: MotionValue) :
    Modifier.Node(), DelegatableNode, CompositionLocalConsumerModifierNode, ObserverModifierNode {
    private var debugger: MotionValueDebugController? = null

    internal var registration: DisposableHandle? = null

    override fun onAttach() {
        debugger = findMotionValueDebugger()
        registration = debugger?.register(motionValue)
        onObservedReadsChanged()
    }

    override fun onDetach() {
@@ -110,6 +104,18 @@ internal class DebugMotionValueNode(motionValue: MotionValue) : Modifier.Node()
        registration?.dispose()
        registration = null
    }

    override fun onObservedReadsChanged() {
        registration?.dispose()
        observeReads { debugger = currentValueOf(LocalMotionValueDebugController) }
        registration = debugger?.register(motionValue)
    }

    var motionValue = motionValue
        set(value) {
            registration = debugger?.register(value)
            field = value
        }
}

private data class DebugMotionValueElement(val motionValue: MotionValue) :
@@ -124,7 +130,3 @@ private data class DebugMotionValueElement(val motionValue: MotionValue) :
        node.motionValue = motionValue
    }
}

internal class MotionValueDebuggerStateImpl : MotionValueDebuggerState {
    override val observedMotionValues: MutableList<MotionValue> = mutableStateListOf()
}
+9 −8
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.mechanics.debug

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -44,11 +45,11 @@ class MotionValueDebuggerTest {

    @Test
    fun debugMotionValue_registersMotionValue_whenAddingToComposition() {
        val debuggerState = MotionValueDebuggerState()
        val debuggerState = MotionValueDebugController()
        var hasValue by mutableStateOf(false)

        rule.setContent {
            Box(modifier = Modifier.motionValueDebugger(debuggerState)) {
            CompositionLocalProvider(LocalMotionValueDebugController provides debuggerState) {
                if (hasValue) {
                    val toDebug = remember {
                        MotionValue(input, gestureContext, { MotionSpec.Empty })
@@ -58,21 +59,21 @@ class MotionValueDebuggerTest {
            }
        }

        assertThat(debuggerState.observedMotionValues).isEmpty()
        assertThat(debuggerState.observed).isEmpty()

        hasValue = true
        rule.waitForIdle()

        assertThat(debuggerState.observedMotionValues).hasSize(1)
        assertThat(debuggerState.observed).hasSize(1)
    }

    @Test
    fun debugMotionValue_unregistersMotionValue_whenLeavingComposition() {
        val debuggerState = MotionValueDebuggerState()
        val debuggerState = MotionValueDebugController()
        var hasValue by mutableStateOf(true)

        rule.setContent {
            Box(modifier = Modifier.motionValueDebugger(debuggerState)) {
            CompositionLocalProvider(LocalMotionValueDebugController provides debuggerState) {
                if (hasValue) {
                    val toDebug = remember {
                        MotionValue(input, gestureContext, { MotionSpec.Empty })
@@ -82,11 +83,11 @@ class MotionValueDebuggerTest {
            }
        }

        assertThat(debuggerState.observedMotionValues).hasSize(1)
        assertThat(debuggerState.observed).hasSize(1)

        hasValue = false
        rule.waitForIdle()
        assertThat(debuggerState.observedMotionValues).isEmpty()
        assertThat(debuggerState.observed).isEmpty()
    }

    @Test