[flexiglass] Drive window view visibility.
When the internal state of our scene system is updated, the visibility of the window view that contains our scene graph is driven by the new startable. Bug: 290656769 Test: added unit tests Test: manually verified that, when the feature flag is enabled, the visibility calls coming from legacy code into WindowRootView#setVisibility are properly ignored and the visibility of the root view is driven by the new system instead. Test: manually verified no harm done. When the feature flag is disabled, the visibility of the window root view is still updated by the legacy code. Change-Id: I20dccfdc7089dda0732b56c384d139e79a4c2f96
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.systemui.scene
|
||||
|
||||
import com.android.systemui.scene.domain.startable.SceneContainerStartableModule
|
||||
import com.android.systemui.scene.shared.model.SceneContainerConfigModule
|
||||
import com.android.systemui.scene.ui.composable.SceneModule
|
||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
|
||||
@@ -25,6 +26,7 @@ import dagger.Module
|
||||
includes =
|
||||
[
|
||||
SceneContainerConfigModule::class,
|
||||
SceneContainerStartableModule::class,
|
||||
SceneContainerViewModelModule::class,
|
||||
SceneModule::class,
|
||||
],
|
||||
|
||||
@@ -24,7 +24,17 @@ import com.android.systemui.scene.shared.model.SceneTransitionModel
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Business logic and app state accessors for the scene framework. */
|
||||
/**
|
||||
* Generic business logic and app state accessors for the scene framework.
|
||||
*
|
||||
* Note that scene container specific business logic does not belong in this class. Instead, it
|
||||
* should be hoisted to a class that is specific to that scene container, for an example, please see
|
||||
* [SystemUiDefaultSceneContainerStartable].
|
||||
*
|
||||
* Also note that this class should not depend on state or logic of other modules or features.
|
||||
* Instead, other feature modules should depend on and call into this class when their parts of the
|
||||
* application state change.
|
||||
*/
|
||||
@SysUISingleton
|
||||
class SceneInteractor
|
||||
@Inject
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 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.startable
|
||||
|
||||
import com.android.systemui.CoreStartable
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
interface SceneContainerStartableModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SystemUiDefaultSceneContainerStartable::class)
|
||||
fun bind(impl: SystemUiDefaultSceneContainerStartable): CoreStartable
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 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.startable
|
||||
|
||||
import com.android.systemui.CoreStartable
|
||||
import com.android.systemui.dagger.SysUISingleton
|
||||
import com.android.systemui.dagger.qualifiers.Application
|
||||
import com.android.systemui.flags.FeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.scene.domain.interactor.SceneInteractor
|
||||
import com.android.systemui.scene.shared.model.SceneContainerNames
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Hooks up business logic that manipulates the state of the [SceneInteractor] for the default
|
||||
* system UI scene container (the one named [SceneContainerNames.SYSTEM_UI_DEFAULT]) based on state
|
||||
* from other systems.
|
||||
*/
|
||||
@SysUISingleton
|
||||
class SystemUiDefaultSceneContainerStartable
|
||||
@Inject
|
||||
constructor(
|
||||
@Application private val applicationScope: CoroutineScope,
|
||||
private val sceneInteractor: SceneInteractor,
|
||||
private val featureFlags: FeatureFlags,
|
||||
) : CoreStartable {
|
||||
|
||||
override fun start() {
|
||||
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
|
||||
keepVisibilityUpdated()
|
||||
}
|
||||
}
|
||||
|
||||
/** Drives visibility of the scene container. */
|
||||
private fun keepVisibilityUpdated() {
|
||||
applicationScope.launch {
|
||||
sceneInteractor
|
||||
.currentScene(CONTAINER_NAME)
|
||||
.map { it.key }
|
||||
.distinctUntilChanged()
|
||||
.collect { sceneKey ->
|
||||
sceneInteractor.setVisible(CONTAINER_NAME, sceneKey != SceneKey.Gone)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CONTAINER_NAME = SceneContainerNames.SYSTEM_UI_DEFAULT
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,10 @@ package com.android.systemui.scene.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.systemui.compose.ComposeFacade
|
||||
import com.android.systemui.lifecycle.repeatWhenAttached
|
||||
import android.view.View
|
||||
import com.android.systemui.scene.shared.model.Scene
|
||||
import com.android.systemui.scene.shared.model.SceneContainerConfig
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** A root view of the main SysUI window that supports scenes. */
|
||||
class SceneWindowRootView(
|
||||
@@ -30,45 +21,19 @@ class SceneWindowRootView(
|
||||
containerConfig: SceneContainerConfig,
|
||||
scenes: Set<Scene>,
|
||||
) {
|
||||
val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
|
||||
val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
|
||||
containerConfig.sceneKeys.forEach { sceneKey ->
|
||||
val scene =
|
||||
checkNotNull(unsortedSceneByKey[sceneKey]) {
|
||||
"Scene not found for key \"$sceneKey\"!"
|
||||
}
|
||||
|
||||
put(sceneKey, scene)
|
||||
SceneWindowRootViewBinder.bind(
|
||||
view = this@SceneWindowRootView,
|
||||
viewModel = viewModel,
|
||||
containerConfig = containerConfig,
|
||||
scenes = scenes,
|
||||
onVisibilityChangedInternal = { isVisible ->
|
||||
super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
repeatWhenAttached {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
setViewTreeOnBackPressedDispatcherOwner(
|
||||
object : OnBackPressedDispatcherOwner {
|
||||
override val onBackPressedDispatcher =
|
||||
OnBackPressedDispatcher().apply {
|
||||
setOnBackInvokedDispatcher(viewRootImpl.onBackInvokedDispatcher)
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle =
|
||||
this@repeatWhenAttached.lifecycle
|
||||
}
|
||||
)
|
||||
|
||||
addView(
|
||||
ComposeFacade.createSceneContainerView(
|
||||
context = context,
|
||||
viewModel = viewModel,
|
||||
sceneByKey = sortedSceneByKey,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Here when destroyed.
|
||||
removeAllViews()
|
||||
}
|
||||
}
|
||||
override fun setVisibility(visibility: Int) {
|
||||
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
|
||||
// visibility from our view-binder.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 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.view
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.OnBackPressedDispatcherOwner
|
||||
import androidx.activity.setViewTreeOnBackPressedDispatcherOwner
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.android.systemui.compose.ComposeFacade
|
||||
import com.android.systemui.lifecycle.repeatWhenAttached
|
||||
import com.android.systemui.scene.shared.model.Scene
|
||||
import com.android.systemui.scene.shared.model.SceneContainerConfig
|
||||
import com.android.systemui.scene.shared.model.SceneKey
|
||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object SceneWindowRootViewBinder {
|
||||
|
||||
/** Binds between the view and view-model pertaining to a specific scene container. */
|
||||
fun bind(
|
||||
view: ViewGroup,
|
||||
viewModel: SceneContainerViewModel,
|
||||
containerConfig: SceneContainerConfig,
|
||||
scenes: Set<Scene>,
|
||||
onVisibilityChangedInternal: (isVisible: Boolean) -> Unit,
|
||||
) {
|
||||
val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
|
||||
val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
|
||||
containerConfig.sceneKeys.forEach { sceneKey ->
|
||||
val scene =
|
||||
checkNotNull(unsortedSceneByKey[sceneKey]) {
|
||||
"Scene not found for key \"$sceneKey\"!"
|
||||
}
|
||||
|
||||
put(sceneKey, scene)
|
||||
}
|
||||
}
|
||||
|
||||
view.repeatWhenAttached {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
view.setViewTreeOnBackPressedDispatcherOwner(
|
||||
object : OnBackPressedDispatcherOwner {
|
||||
override val onBackPressedDispatcher =
|
||||
OnBackPressedDispatcher().apply {
|
||||
setOnBackInvokedDispatcher(
|
||||
view.viewRootImpl.onBackInvokedDispatcher
|
||||
)
|
||||
}
|
||||
|
||||
override val lifecycle: Lifecycle = this@repeatWhenAttached.lifecycle
|
||||
}
|
||||
)
|
||||
|
||||
view.addView(
|
||||
ComposeFacade.createSceneContainerView(
|
||||
context = view.context,
|
||||
viewModel = viewModel,
|
||||
sceneByKey = sortedSceneByKey,
|
||||
)
|
||||
)
|
||||
|
||||
launch {
|
||||
viewModel.isVisible.collect { isVisible ->
|
||||
onVisibilityChangedInternal(isVisible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Here when destroyed.
|
||||
view.removeAllViews()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 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.startable
|
||||
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.coroutines.collectLastValue
|
||||
import com.android.systemui.flags.FakeFeatureFlags
|
||||
import com.android.systemui.flags.Flags
|
||||
import com.android.systemui.scene.SceneTestUtils
|
||||
import com.android.systemui.scene.shared.model.SceneContainerNames
|
||||
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 SystemUiDefaultSceneContainerStartableTest : SysuiTestCase() {
|
||||
|
||||
private val utils = SceneTestUtils(this)
|
||||
private val testScope = utils.testScope
|
||||
private val sceneInteractor = utils.sceneInteractor()
|
||||
private val featureFlags = FakeFeatureFlags()
|
||||
|
||||
private val underTest =
|
||||
SystemUiDefaultSceneContainerStartable(
|
||||
applicationScope = testScope.backgroundScope,
|
||||
sceneInteractor = sceneInteractor,
|
||||
featureFlags = featureFlags,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun start_featureEnabled_keepsVisibilityUpdated() =
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.SCENE_CONTAINER, true)
|
||||
val isVisible by
|
||||
collectLastValue(sceneInteractor.isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT))
|
||||
assertThat(isVisible).isTrue()
|
||||
|
||||
underTest.start()
|
||||
|
||||
sceneInteractor.setCurrentScene(
|
||||
SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||
SceneModel(SceneKey.Gone)
|
||||
)
|
||||
assertThat(isVisible).isFalse()
|
||||
|
||||
sceneInteractor.setCurrentScene(
|
||||
SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||
SceneModel(SceneKey.Shade)
|
||||
)
|
||||
assertThat(isVisible).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun start_featureDisabled_doesNotUpdateVisibility() =
|
||||
testScope.runTest {
|
||||
featureFlags.set(Flags.SCENE_CONTAINER, false)
|
||||
val isVisible by
|
||||
collectLastValue(sceneInteractor.isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT))
|
||||
assertThat(isVisible).isTrue()
|
||||
|
||||
underTest.start()
|
||||
|
||||
sceneInteractor.setCurrentScene(
|
||||
SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||
SceneModel(SceneKey.Gone)
|
||||
)
|
||||
assertThat(isVisible).isTrue()
|
||||
|
||||
sceneInteractor.setCurrentScene(
|
||||
SceneContainerNames.SYSTEM_UI_DEFAULT,
|
||||
SceneModel(SceneKey.Shade)
|
||||
)
|
||||
assertThat(isVisible).isTrue()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user