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

Commit 1dff293f authored by Ale Nijamkin's avatar Ale Nijamkin Committed by Android (Google) Code Review
Browse files

Merge "[flexiglass] SceneDataSource." into main

parents 7f45215f b70482d4
Loading
Loading
Loading
Loading
+103 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.ui.composable

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.IntSize
import com.android.compose.animation.scene.Back
import com.android.compose.animation.scene.Edge as ComposeAwareEdge
import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.SwipeDirection
import com.android.compose.animation.scene.TransitionKey as ComposeAwareTransitionKey
import com.android.compose.animation.scene.UserAction as ComposeAwareUserAction
import com.android.compose.animation.scene.UserActionDistance as ComposeAwareUserActionDistance
import com.android.compose.animation.scene.UserActionResult as ComposeAwareUserActionResult
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.Edge
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.TransitionKey
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.shared.model.UserActionDistance
import com.android.systemui.scene.shared.model.UserActionResult

// TODO(b/293899074): remove this file once we can use the types from SceneTransitionLayout.

fun SceneKey.asComposeAware(): ComposeAwareSceneKey {
    return ComposeAwareSceneKey(
        debugName = toString(),
        identity = this,
    )
}

fun TransitionKey.asComposeAware(): ComposeAwareTransitionKey {
    return ComposeAwareTransitionKey(
        debugName = debugName,
        identity = this,
    )
}

fun UserAction.asComposeAware(): ComposeAwareUserAction {
    return when (this) {
        is UserAction.Swipe ->
            Swipe(
                pointerCount = pointerCount,
                fromSource =
                    when (this.fromEdge) {
                        null -> null
                        Edge.LEFT -> ComposeAwareEdge.Left
                        Edge.TOP -> ComposeAwareEdge.Top
                        Edge.RIGHT -> ComposeAwareEdge.Right
                        Edge.BOTTOM -> ComposeAwareEdge.Bottom
                    },
                direction =
                    when (this.direction) {
                        Direction.LEFT -> SwipeDirection.Left
                        Direction.UP -> SwipeDirection.Up
                        Direction.RIGHT -> SwipeDirection.Right
                        Direction.DOWN -> SwipeDirection.Down
                    }
            )
        is UserAction.Back -> Back
    }
}

fun UserActionResult.asComposeAware(): ComposeAwareUserActionResult {
    val composeUnaware = this
    return ComposeAwareUserActionResult(
        toScene = composeUnaware.toScene.asComposeAware(),
        transitionKey = composeUnaware.transitionKey?.asComposeAware(),
        distance = composeUnaware.distance?.asComposeAware(),
    )
}

fun UserActionDistance.asComposeAware(): ComposeAwareUserActionDistance {
    val composeUnware = this
    return object : ComposeAwareUserActionDistance {
        override fun Density.absoluteDistance(
            fromSceneSize: IntSize,
            orientation: Orientation,
        ): Float {
            return composeUnware.absoluteDistance(
                fromSceneWidth = fromSceneSize.width,
                fromSceneHeight = fromSceneSize.height,
                isHorizontal = orientation == Orientation.Horizontal,
            )
        }
    }
}
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.ui.composable

import com.android.compose.animation.scene.ObservableTransitionState as ComposeAwareObservableTransitionState
import com.android.compose.animation.scene.SceneKey as ComposeAwareSceneKey
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.SceneKey

fun ComposeAwareSceneKey.asComposeUnaware(): SceneKey {
    return this.identity as SceneKey
}

fun ComposeAwareObservableTransitionState.asComposeUnaware(): ObservableTransitionState {
    return when (this) {
        is ComposeAwareObservableTransitionState.Idle ->
            ObservableTransitionState.Idle(scene.asComposeUnaware())
        is ComposeAwareObservableTransitionState.Transition ->
            ObservableTransitionState.Transition(
                fromScene = fromScene.asComposeUnaware(),
                toScene = toScene.asComposeUnaware(),
                progress = progress,
                isInitiatedByUserInput = isInitiatedByUserInput,
                isUserInputOngoing = isUserInputOngoing,
            )
    }
}
+81 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(ExperimentalCoroutinesApi::class)

package com.android.systemui.scene.ui.composable

import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.observableTransitionState
import com.android.systemui.scene.shared.model.SceneDataSource
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.TransitionKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

/**
 * An implementation of [SceneDataSource] that's backed by a [MutableSceneTransitionLayoutState].
 */
class SceneTransitionLayoutDataSource(
    private val state: MutableSceneTransitionLayoutState,

    /**
     * The [CoroutineScope] of the @Composable that's using this, it's critical that this is *not*
     * the application scope.
     */
    private val coroutineScope: CoroutineScope,
) : SceneDataSource {
    override val currentScene: StateFlow<SceneKey> =
        state
            .observableTransitionState()
            .flatMapLatest { observableTransitionState ->
                when (observableTransitionState) {
                    is ObservableTransitionState.Idle -> flowOf(observableTransitionState.scene)
                    is ObservableTransitionState.Transition ->
                        observableTransitionState.isUserInputOngoing.map { isUserInputOngoing ->
                            if (isUserInputOngoing) {
                                observableTransitionState.fromScene
                            } else {
                                observableTransitionState.toScene
                            }
                        }
                }
            }
            .map { it.asComposeUnaware() }
            .stateIn(
                scope = coroutineScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = state.transitionState.currentScene.asComposeUnaware(),
            )

    override fun changeScene(
        toScene: SceneKey,
        transitionKey: TransitionKey?,
    ) {
        state.setTargetScene(
            targetScene = toScene.asComposeAware(),
            transitionKey = transitionKey?.asComposeAware(),
            coroutineScope = coroutineScope,
        )
    }
}
+90 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.shared.model

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.initialSceneKey
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SceneDataSourceDelegatorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val initialSceneKey = kosmos.initialSceneKey
    private val fakeSceneDataSource = kosmos.fakeSceneDataSource

    private val underTest = kosmos.sceneDataSourceDelegator

    @Test
    fun currentScene_withoutDelegate_startsWithInitialScene() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            underTest.setDelegate(null)

            assertThat(currentScene).isEqualTo(initialSceneKey)
        }

    @Test
    fun currentScene_withoutDelegate_doesNothing() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            underTest.setDelegate(null)
            assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)

            underTest.changeScene(toScene = SceneKey.Bouncer)

            assertThat(currentScene).isEqualTo(initialSceneKey)
        }

    @Test
    fun currentScene_withDelegate_startsWithInitialScene() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            assertThat(currentScene).isEqualTo(initialSceneKey)
        }

    @Test
    fun currentScene_withDelegate_changesScenes() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)
            assertThat(currentScene).isNotEqualTo(SceneKey.Bouncer)

            underTest.changeScene(toScene = SceneKey.Bouncer)

            assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
        }

    @Test
    fun currentScene_reflectsDelegate() =
        testScope.runTest {
            val currentScene by collectLastValue(underTest.currentScene)

            fakeSceneDataSource.changeScene(toScene = SceneKey.Bouncer)

            assertThat(currentScene).isEqualTo(SceneKey.Bouncer)
        }
}
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.scene.shared.model

import kotlinx.coroutines.flow.StateFlow

/** Defines interface for classes that provide access to scene state. */
interface SceneDataSource {

    /**
     * The current scene, as seen by the real data source in the UI layer.
     *
     * During a transition between two scenes, the original scene will still be reflected in
     * [currentScene] until a time when the UI layer decides to commit the change, which is when
     * [currentScene] will have the value of the target/new scene.
     */
    val currentScene: StateFlow<SceneKey>

    /**
     * Asks for an asynchronous scene switch to [toScene], which will use the corresponding
     * installed transition or the one specified by [transitionKey], if provided.
     */
    fun changeScene(
        toScene: SceneKey,
        transitionKey: TransitionKey? = null,
    )
}
Loading