Merge "[flexiglass] Drive window view visibility." into udc-qpr-dev

This commit is contained in:
Ale Nijamkin
2023-07-11 04:13:59 +00:00
committed by Android (Google) Code Review
7 changed files with 317 additions and 49 deletions

View File

@@ -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,
],

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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.
}
}

View File

@@ -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()
}
}
}
}

View File

@@ -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()
}
}