Loading packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt 0 → 100644 +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, ) } } } packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt 0 → 100644 +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, ) } } packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt 0 → 100644 +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, ) } } packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt 0 → 100644 +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) } } packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt 0 → 100644 +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
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeAwareExtensions.kt 0 → 100644 +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, ) } } }
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/ComposeUnawareExtensions.kt 0 → 100644 +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, ) } }
packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneTransitionLayoutDataSource.kt 0 → 100644 +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, ) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegatorTest.kt 0 → 100644 +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) } }
packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSource.kt 0 → 100644 +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, ) }