Merge "[flexiglass] Drive window view visibility." into udc-qpr-dev
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package com.android.systemui.scene
|
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.shared.model.SceneContainerConfigModule
|
||||||
import com.android.systemui.scene.ui.composable.SceneModule
|
import com.android.systemui.scene.ui.composable.SceneModule
|
||||||
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
|
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
|
||||||
@@ -25,6 +26,7 @@ import dagger.Module
|
|||||||
includes =
|
includes =
|
||||||
[
|
[
|
||||||
SceneContainerConfigModule::class,
|
SceneContainerConfigModule::class,
|
||||||
|
SceneContainerStartableModule::class,
|
||||||
SceneContainerViewModelModule::class,
|
SceneContainerViewModelModule::class,
|
||||||
SceneModule::class,
|
SceneModule::class,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -24,7 +24,17 @@ import com.android.systemui.scene.shared.model.SceneTransitionModel
|
|||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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
|
@SysUISingleton
|
||||||
class SceneInteractor
|
class SceneInteractor
|
||||||
@Inject
|
@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.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.activity.OnBackPressedDispatcher
|
import android.view.View
|
||||||
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.Scene
|
||||||
import com.android.systemui.scene.shared.model.SceneContainerConfig
|
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 com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/** A root view of the main SysUI window that supports scenes. */
|
/** A root view of the main SysUI window that supports scenes. */
|
||||||
class SceneWindowRootView(
|
class SceneWindowRootView(
|
||||||
@@ -30,45 +21,19 @@ class SceneWindowRootView(
|
|||||||
containerConfig: SceneContainerConfig,
|
containerConfig: SceneContainerConfig,
|
||||||
scenes: Set<Scene>,
|
scenes: Set<Scene>,
|
||||||
) {
|
) {
|
||||||
val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
|
SceneWindowRootViewBinder.bind(
|
||||||
val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
|
view = this@SceneWindowRootView,
|
||||||
containerConfig.sceneKeys.forEach { sceneKey ->
|
viewModel = viewModel,
|
||||||
val scene =
|
containerConfig = containerConfig,
|
||||||
checkNotNull(unsortedSceneByKey[sceneKey]) {
|
scenes = scenes,
|
||||||
"Scene not found for key \"$sceneKey\"!"
|
onVisibilityChangedInternal = { isVisible ->
|
||||||
}
|
super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE)
|
||||||
|
|
||||||
put(sceneKey, scene)
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
repeatWhenAttached {
|
override fun setVisibility(visibility: Int) {
|
||||||
lifecycleScope.launch {
|
// Do nothing. We don't want external callers to invoke this. Instead, we drive our own
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
// visibility from our view-binder.
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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