Merge "[flexiglass] Drive window view visibility." into udc-qpr-dev
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