From ceb61251694132b06067ece8414f6925cf74de28 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 24 Apr 2023 15:42:41 -0700 Subject: [PATCH 1/3] [flexiglass] Container config - data layer. The container configuration portion of the Flexiglass framework allows OEMs to configure one or more Flexiglass containers with scenes. Each scene is a distinct bit of UI content that the user can navigate to (for example by swiping in a direction) or that can be navigated to automatically through the SceneInteractor methods. Bug: 279501596 Test: Included unit tests. Manually tested through a testbed app that uses Jetpack Compose to implement a simple UI to traverse all scenes. Change-Id: I71bc7ea373d23312e3b4e861fa061a3cd825e938 --- .../scene/data/model/SceneContainerConfig.kt | 46 ++++++ .../repository/SceneContainerRepository.kt | 136 +++++++++++++++++ .../systemui/scene/shared/model/Scene.kt | 87 +++++++++++ .../systemui/scene/shared/model/SceneKey.kt | 49 ++++++ .../systemui/scene/shared/model/SceneModel.kt | 27 ++++ .../systemui/scene/data/repository/Fakes.kt | 51 +++++++ .../SceneContainerRepositoryTest.kt | 139 ++++++++++++++++++ 7 files changed, 535 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt create mode 100644 packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt create mode 100644 packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt create mode 100644 packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt new file mode 100644 index 0000000000000..d0769ebe941ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/data/model/SceneContainerConfig.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.data.model + +import com.android.systemui.scene.shared.model.SceneKey + +/** Models the configuration of a single scene container. */ +data class SceneContainerConfig( + /** Container name. Must be unique across all containers in System UI. */ + val name: String, + + /** + * The keys to all scenes in the container, sorted by z-order such that the last one renders on + * top of all previous ones. Scene keys within the same container must not repeat but it's okay + * to have the same scene keys in different containers. + */ + val sceneKeys: List, + + /** + * The key of the scene that is the initial current scene when the container is first set up, + * before taking any application state in to account. + */ + val initialSceneKey: SceneKey, +) { + init { + check(sceneKeys.isNotEmpty()) { "A container must have at least one scene key." } + + check(sceneKeys.contains(initialSceneKey)) { + "The initial key \"$initialSceneKey\" is not present in this container." + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt new file mode 100644 index 0000000000000..61b162b014d82 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/data/repository/SceneContainerRepository.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2023 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.data.repository + +import com.android.systemui.scene.data.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Source of truth for scene framework application state. */ +class SceneContainerRepository +@Inject +constructor( + containerConfigurations: Set, +) { + + private val containerConfigByName: Map = + containerConfigurations.associateBy { config -> config.name } + private val containerVisibilityByName: Map> = + containerConfigByName + .map { (containerName, _) -> containerName to MutableStateFlow(true) } + .toMap() + private val currentSceneByContainerName: Map> = + containerConfigByName + .map { (containerName, config) -> + containerName to MutableStateFlow(SceneModel(config.initialSceneKey)) + } + .toMap() + private val sceneTransitionProgressByContainerName: Map> = + containerConfigByName + .map { (containerName, _) -> containerName to MutableStateFlow(1f) } + .toMap() + + init { + val repeatedContainerNames = + containerConfigurations + .groupingBy { config -> config.name } + .eachCount() + .filter { (_, count) -> count > 1 } + check(repeatedContainerNames.isEmpty()) { + "Container names must be unique. The following container names appear more than once: ${ + repeatedContainerNames + .map { (name, count) -> "\"$name\" appears $count times" } + .joinToString(", ") + }" + } + } + + /** + * Returns the keys to all scenes in the container with the given name. + * + * The scenes will be sorted in z-order such that the last one is the one that should be + * rendered on top of all previous ones. + */ + fun allSceneKeys(containerName: String): List { + return containerConfigByName[containerName]?.sceneKeys + ?: error(noSuchContainerErrorMessage(containerName)) + } + + /** Sets the current scene in the container with the given name. */ + fun setCurrentScene(containerName: String, scene: SceneModel) { + check(allSceneKeys(containerName).contains(scene.key)) { + """ + Cannot set current scene key to "${scene.key}". The container "$containerName" does + not contain a scene with that key. + """ + .trimIndent() + } + + currentSceneByContainerName.setValue(containerName, scene) + } + + /** The current scene in the container with the given name. */ + fun currentScene(containerName: String): StateFlow { + return currentSceneByContainerName.mutableOrError(containerName).asStateFlow() + } + + /** Sets whether the container with the given name is visible. */ + fun setVisible(containerName: String, isVisible: Boolean) { + containerVisibilityByName.setValue(containerName, isVisible) + } + + /** Whether the container with the given name should be visible. */ + fun isVisible(containerName: String): StateFlow { + return containerVisibilityByName.mutableOrError(containerName).asStateFlow() + } + + /** Sets scene transition progress to the current scene in the container with the given name. */ + fun setSceneTransitionProgress(containerName: String, progress: Float) { + sceneTransitionProgressByContainerName.setValue(containerName, progress) + } + + /** Progress of the transition into the current scene in the container with the given name. */ + fun sceneTransitionProgress(containerName: String): StateFlow { + return sceneTransitionProgressByContainerName.mutableOrError(containerName).asStateFlow() + } + + private fun Map>.mutableOrError( + containerName: String, + ): MutableStateFlow { + return this[containerName] ?: error(noSuchContainerErrorMessage(containerName)) + } + + private fun Map>.setValue( + containerName: String, + value: T, + ) { + val mutable = mutableOrError(containerName) + mutable.value = value + } + + private fun noSuchContainerErrorMessage(containerName: String): String { + return """ + No container named "$containerName". Existing containers: + ${containerConfigByName.values.joinToString(", ") { it.name }} + """ + .trimIndent() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt new file mode 100644 index 0000000000000..435ff4baffd85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scene.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 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.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Defines interface for classes that can describe a "scene". + * + * In the scene framework, there can be multiple scenes in a single scene "container". The container + * takes care of rendering the current scene and allowing scenes to be switched from one to another + * based on either user action (for example, swiping down while on the lock screen scene may switch + * to the shade scene). + * + * The framework also supports multiple containers, each one with its own configuration. + */ +interface Scene { + + /** Uniquely-identifying key for this scene. The key must be unique within its container. */ + val key: SceneKey + + /** + * Returns a mapping between [UserAction] and flows that emit a [SceneModel]. + * + * When the scene framework detects the user action, it starts a transition to the scene + * described by the latest value in the flow that's mapped from that user action. + * + * Once the [Scene] becomes the current one, the scene framework will invoke this method and set + * up collectors to watch for new values emitted to each of the flows. If a value is added to + * the map at a given [UserAction], the framework will set up user input handling for that + * [UserAction] and, if such a user action is detected, the framework will initiate a transition + * to that [SceneModel]. + * + * Note that calling this method does _not_ mean that the given user action has occurred. + * Instead, the method is called before any user action/gesture is detected so that the + * framework can decide whether to set up gesture/input detectors/listeners for that type of + * user action. + * + * Note that a missing value for a specific [UserAction] means that the user action of the given + * type is not currently active in the scene and should be ignored by the framework, while the + * current scene is this one. + * + * The API is designed such that it's possible to emit ever-changing values for each + * [UserAction] to enable, disable, or change the destination scene of a given user action. + */ + fun destinationScenes(): StateFlow> = + MutableStateFlow(emptyMap()).asStateFlow() +} + +/** Enumerates all scene framework supported user actions. */ +sealed interface UserAction { + + /** The user is scrolling, dragging, swiping, or flinging. */ + data class Swipe( + /** The direction of the swipe. */ + val direction: Direction, + /** The number of pointers that were used (for example, one or two fingers). */ + val pointerCount: Int = 1, + ) : UserAction + + /** The user has hit the back button or performed the back navigation gesture. */ + object Back : UserAction +} + +/** Enumerates all known "cardinal" directions for user actions. */ +enum class Direction { + LEFT, + UP, + RIGHT, + DOWN, +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt new file mode 100644 index 0000000000000..9ef439d72118d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneKey.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2023 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 + +/** Keys of all known scenes. */ +sealed class SceneKey( + private val loggingName: String, +) { + /** + * The bouncer is the scene that displays authentication challenges like PIN, password, or + * pattern. + */ + object Bouncer : SceneKey("bouncer") + + /** + * "Gone" is not a real scene but rather the absence of scenes when we want to skip showing any + * content from the scene framework. + */ + object Gone : SceneKey("gone") + + /** The lock screen is the scene that shows when the device is locked. */ + object LockScreen : SceneKey("lockscreen") + + /** + * The shade is the scene whose primary purpose is to show a scrollable list of notifications. + */ + object Shade : SceneKey("shade") + + /** The quick settings scene shows the quick setting tiles. */ + object QuickSettings : SceneKey("quick_settings") + + override fun toString(): String { + return loggingName + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt new file mode 100644 index 0000000000000..f3d549f038685 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 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 + +/** Models a scene. */ +data class SceneModel( + + /** The key of the scene. */ + val key: SceneKey, + + /** An optional name for the transition that led to this scene being the current scene. */ + val transitionName: String? = null, +) diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt new file mode 100644 index 0000000000000..1cdaec0c6581c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/Fakes.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 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.data.repository + +import com.android.systemui.scene.data.model.SceneContainerConfig +import com.android.systemui.scene.shared.model.SceneKey + +fun fakeSceneContainerRepository( + containerConfigurations: Set = + setOf( + fakeSceneContainerConfig("container1"), + fakeSceneContainerConfig("container2"), + ) +): SceneContainerRepository { + return SceneContainerRepository(containerConfigurations) +} + +fun fakeSceneKeys(): List { + return listOf( + SceneKey.QuickSettings, + SceneKey.Shade, + SceneKey.LockScreen, + SceneKey.Bouncer, + SceneKey.Gone, + ) +} + +fun fakeSceneContainerConfig( + name: String, + sceneKeys: List = fakeSceneKeys(), +): SceneContainerConfig { + return SceneContainerConfig( + name = name, + sceneKeys = sceneKeys, + initialSceneKey = SceneKey.LockScreen, + ) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt new file mode 100644 index 0000000000000..9e264db845e1c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2023 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.data.repository + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class SceneContainerRepositoryTest : SysuiTestCase() { + + @Test + fun allSceneKeys() { + val underTest = fakeSceneContainerRepository() + assertThat(underTest.allSceneKeys("container1")) + .isEqualTo( + listOf( + SceneKey.QuickSettings, + SceneKey.Shade, + SceneKey.LockScreen, + SceneKey.Bouncer, + SceneKey.Gone, + ) + ) + } + + @Test(expected = IllegalStateException::class) + fun allSceneKeys_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.allSceneKeys("nonExistingContainer") + } + + @Test + fun currentScene() = runTest { + val underTest = fakeSceneContainerRepository() + val currentScene by collectLastValue(underTest.currentScene("container1")) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) + } + + @Test(expected = IllegalStateException::class) + fun currentScene_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.currentScene("nonExistingContainer") + } + + @Test(expected = IllegalStateException::class) + fun setCurrentScene_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.setCurrentScene("nonExistingContainer", SceneModel(SceneKey.Shade)) + } + + @Test(expected = IllegalStateException::class) + fun setCurrentScene_noSuchSceneInContainer_throws() { + val underTest = + fakeSceneContainerRepository( + setOf( + fakeSceneContainerConfig("container1"), + fakeSceneContainerConfig( + "container2", + listOf(SceneKey.QuickSettings, SceneKey.LockScreen) + ), + ) + ) + underTest.setCurrentScene("container2", SceneModel(SceneKey.Shade)) + } + + @Test + fun isVisible() = runTest { + val underTest = fakeSceneContainerRepository() + val isVisible by collectLastValue(underTest.isVisible("container1")) + assertThat(isVisible).isTrue() + + underTest.setVisible("container1", false) + assertThat(isVisible).isFalse() + + underTest.setVisible("container1", true) + assertThat(isVisible).isTrue() + } + + @Test(expected = IllegalStateException::class) + fun isVisible_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.isVisible("nonExistingContainer") + } + + @Test(expected = IllegalStateException::class) + fun setVisible_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.setVisible("nonExistingContainer", false) + } + + @Test + fun sceneTransitionProgress() = runTest { + val underTest = fakeSceneContainerRepository() + val sceneTransitionProgress by + collectLastValue(underTest.sceneTransitionProgress("container1")) + assertThat(sceneTransitionProgress).isEqualTo(1f) + + underTest.setSceneTransitionProgress("container1", 0.1f) + assertThat(sceneTransitionProgress).isEqualTo(0.1f) + + underTest.setSceneTransitionProgress("container1", 0.9f) + assertThat(sceneTransitionProgress).isEqualTo(0.9f) + } + + @Test(expected = IllegalStateException::class) + fun sceneTransitionProgress_noSuchContainer_throws() { + val underTest = fakeSceneContainerRepository() + underTest.sceneTransitionProgress("nonExistingContainer") + } +} From 898bcd5c914ae8c64bdcf1be2fc872134e19c708 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Mon, 24 Apr 2023 15:48:15 -0700 Subject: [PATCH 2/3] [flexiglass] Container config - domain layer. The container configuration portion of the Flexiglass framework allows OEMs to configure one or more Flexiglass containers with scenes. Each scene is a distinct bit of UI content that the user can navigate to (for example by swiping in a direction) or that can be navigated to automatically through the SceneInteractor methods. Bug: 279501596 Test: Included unit tests. Manually tested through a testbed app that uses Jetpack Compose to implement a simple UI to traverse all scenes. Change-Id: I28708a55963ae79d48e91f2dcef06eeadea167c4 --- .../domain/interactor/SceneInteractor.kt | 73 +++++++++++++++++ .../domain/interactor/SceneInteractorTest.kt | 78 +++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt new file mode 100644 index 0000000000000..1e55975658a5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 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.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +/** Business logic and app state accessors for the scene framework. */ +@SysUISingleton +class SceneInteractor +@Inject +constructor( + private val repository: SceneContainerRepository, +) { + + /** + * Returns the keys of all scenes in the container with the given name. + * + * The scenes will be sorted in z-order such that the last one is the one that should be + * rendered on top of all previous ones. + */ + fun allSceneKeys(containerName: String): List { + return repository.allSceneKeys(containerName) + } + + /** Sets the scene in the container with the given name. */ + fun setCurrentScene(containerName: String, scene: SceneModel) { + repository.setCurrentScene(containerName, scene) + } + + /** The current scene in the container with the given name. */ + fun currentScene(containerName: String): StateFlow { + return repository.currentScene(containerName) + } + + /** Sets the visibility of the container with the given name. */ + fun setVisible(containerName: String, isVisible: Boolean) { + return repository.setVisible(containerName, isVisible) + } + + /** Whether the container with the given name is visible. */ + fun isVisible(containerName: String): StateFlow { + return repository.isVisible(containerName) + } + + /** Sets scene transition progress to the current scene in the container with the given name. */ + fun setSceneTransitionProgress(containerName: String, progress: Float) { + repository.setSceneTransitionProgress(containerName, progress) + } + + /** Progress of the transition into the current scene in the container with the given name. */ + fun sceneTransitionProgress(containerName: String): StateFlow { + return repository.sceneTransitionProgress(containerName) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt new file mode 100644 index 0000000000000..c5ce092468627 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 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, ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.data.repository.fakeSceneKeys +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class SceneInteractorTest : SysuiTestCase() { + + private val underTest = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + + @Test + fun allSceneKeys() { + assertThat(underTest.allSceneKeys("container1")).isEqualTo(fakeSceneKeys()) + } + + @Test + fun sceneTransitions() = runTest { + val currentScene by collectLastValue(underTest.currentScene("container1")) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.setCurrentScene("container1", SceneModel(SceneKey.Shade)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) + } + + @Test + fun sceneTransitionProgress() = runTest { + val progress by collectLastValue(underTest.sceneTransitionProgress("container1")) + assertThat(progress).isEqualTo(1f) + + underTest.setSceneTransitionProgress("container1", 0.55f) + assertThat(progress).isEqualTo(0.55f) + } + + @Test + fun isVisible() = runTest { + val isVisible by collectLastValue(underTest.isVisible("container1")) + assertThat(isVisible).isTrue() + + underTest.setVisible("container1", false) + assertThat(isVisible).isFalse() + + underTest.setVisible("container1", true) + assertThat(isVisible).isTrue() + } +} From 213e2ad4758d3e91bde646267ded95b0d38d7996 Mon Sep 17 00:00:00 2001 From: Alejandro Nijamkin Date: Tue, 9 May 2023 09:49:13 -0700 Subject: [PATCH 3/3] [flexiglass] Container config - UI layer. The container configuration portion of the Flexiglass framework allows OEMs to configure one or more Flexiglass containers with scenes. Each scene is a distinct bit of UI content that the user can navigate to (for example by swiping in a direction) or that can be navigated to automatically through the SceneInteractor methods. Bug: 279501596 Test: Included unit tests. Manually tested through a testbed app that uses Jetpack Compose to implement a simple UI to traverse all scenes. Change-Id: I2c925707b0709e4aaf26bca8f94e5e2b3cf6bbcc --- .../ui/viewmodel/SceneContainerViewModel.kt | 64 ++++++++++++++++ .../viewmodel/SceneContainerViewModelTest.kt | 74 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt new file mode 100644 index 0000000000000..afc053151ab5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2023 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.viewmodel + +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow + +/** Models UI state for a single scene container. */ +class SceneContainerViewModel +@AssistedInject +constructor( + private val interactor: SceneInteractor, + @Assisted private val containerName: String, +) { + /** + * Keys of all scenes in the container. + * + * The scenes will be sorted in z-order such that the last one is the one that should be + * rendered on top of all previous ones. + */ + val allSceneKeys: List = interactor.allSceneKeys(containerName) + + /** The current scene. */ + val currentScene: StateFlow = interactor.currentScene(containerName) + + /** Whether the container is visible. */ + val isVisible: StateFlow = interactor.isVisible(containerName) + + /** Requests a transition to the scene with the given key. */ + fun setCurrentScene(scene: SceneModel) { + interactor.setCurrentScene(containerName, scene) + } + + /** Notifies of the progress of a scene transition. */ + fun setSceneTransitionProgress(progress: Float) { + interactor.setSceneTransitionProgress(containerName, progress) + } + + @AssistedFactory + interface Factory { + fun create( + containerName: String, + ): SceneContainerViewModel + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt new file mode 100644 index 0000000000000..ab61ddddaeab5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2023 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.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.scene.data.repository.fakeSceneContainerRepository +import com.android.systemui.scene.data.repository.fakeSceneKeys +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class SceneContainerViewModelTest : SysuiTestCase() { + private val interactor = + SceneInteractor( + repository = fakeSceneContainerRepository(), + ) + private val underTest = + SceneContainerViewModel( + interactor = interactor, + containerName = "container1", + ) + + @Test + fun isVisible() = runTest { + val isVisible by collectLastValue(underTest.isVisible) + assertThat(isVisible).isTrue() + + interactor.setVisible("container1", false) + assertThat(isVisible).isFalse() + + interactor.setVisible("container1", true) + assertThat(isVisible).isTrue() + } + + @Test + fun allSceneKeys() { + assertThat(underTest.allSceneKeys).isEqualTo(fakeSceneKeys()) + } + + @Test + fun sceneTransition() = runTest { + val currentScene by collectLastValue(underTest.currentScene) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.LockScreen)) + + underTest.setCurrentScene(SceneModel(SceneKey.Shade)) + assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) + } +}