[flexiglass] SceneContainer in System UI.

If the Jetpack Compose compile-time flag and the scene container runtime
feature flag are both true, when we inflate the root
SceneWindowRootView, we also initialize it with its required
dependencies and add a ComposeView that hosts the SceneContainer
composable function.

This effectively adds the placholder scenes developed through the
Compose gallery app to actual System UI, allowing us to proceed to the
next steps of the project where we being connecting Flexiglass to real
business logic and application state.

Note that the back navigation integration done requires the enablement
of predictive back animation for the shade as described in
go/enable-notification-shade-anim.

Bug: 283836103
Test: manually verified that the Flexiglass placholder scenes correctly
show on top of the shade, correctly hide when the window is gone, can
correctly be navigated between using the placholder up/down buttons, and
the bouncer scene correctly supports the back gesture to go back to
lockscreen.

Change-Id: I82c864e922ce77469ae7fbc5f0cef77e6947ef3f
This commit is contained in:
Alejandro Nijamkin
2023-06-08 16:50:20 -07:00
committed by Ale Nijamkin
parent b7de325e66
commit faaf228ad4
16 changed files with 224 additions and 50 deletions

View File

@@ -24,6 +24,9 @@ import androidx.lifecycle.LifecycleOwner
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is *not* available. */
@@ -58,6 +61,14 @@ object ComposeFacade : BaseComposeFacade {
throwComposeUnavailableError()
}
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
sceneByKey: Map<SceneKey, Scene>,
): View {
throwComposeUnavailableError()
}
private fun throwComposeUnavailableError(): Nothing {
error(
"Compose is not available. Make sure to check isComposeAvailable() before calling any" +

View File

@@ -29,6 +29,11 @@ import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/** The Compose facade, when Compose is available. */
@@ -71,4 +76,22 @@ object ComposeFacade : BaseComposeFacade {
}
}
}
override fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
sceneByKey: Map<SceneKey, Scene>,
): View {
return ComposeView(context).apply {
setContent {
PlatformTheme {
SceneContainer(
viewModel = viewModel,
sceneByKey =
sceneByKey.mapValues { (_, scene) -> scene as ComposableScene },
)
}
}
}
}
}

View File

@@ -24,6 +24,9 @@ import androidx.lifecycle.LifecycleOwner
import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.util.time.SystemClock
/**
@@ -66,4 +69,11 @@ interface BaseComposeFacade {
viewModel: MultiShadeViewModel,
clock: SystemClock,
): View
/** Create a [View] to represent [viewModel] on screen. */
fun createSceneContainerView(
context: Context,
viewModel: SceneContainerViewModel,
sceneByKey: Map<SceneKey, Scene>,
): View
}

View File

@@ -18,6 +18,7 @@ package com.android.systemui.dagger;
import com.android.systemui.globalactions.ShutdownUiModule;
import com.android.systemui.keyguard.CustomizationProvider;
import com.android.systemui.scene.startable.SceneContainerStartableModule;
import com.android.systemui.statusbar.NotificationInsetsModule;
import com.android.systemui.statusbar.QsFrameTranslateModule;
@@ -33,6 +34,7 @@ import dagger.Subcomponent;
NotificationInsetsModule.class,
QsFrameTranslateModule.class,
ShutdownUiModule.class,
SceneContainerStartableModule.class,
SystemUIBinder.class,
SystemUIModule.class,
SystemUICoreStartableModule.class,

View File

@@ -40,7 +40,6 @@ import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.recents.Recents;
import com.android.systemui.recents.RecentsImplementation;
import com.android.systemui.rotationlock.RotationLockModule;
import com.android.systemui.scene.SceneContainerFrameworkModule;
import com.android.systemui.screenshot.ReferenceScreenshotModule;
import com.android.systemui.settings.dagger.MultiUserUtilsModule;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
@@ -104,7 +103,6 @@ import javax.inject.Named;
QSModule.class,
ReferenceScreenshotModule.class,
RotationLockModule.class,
SceneContainerFrameworkModule.class,
StatusBarEventsModule.class,
StartCentralSurfacesModule.class,
VolumeModule.class,

View File

@@ -38,6 +38,7 @@ import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.biometrics.UdfpsDisplayModeProvider;
import com.android.systemui.biometrics.dagger.BiometricsModule;
import com.android.systemui.biometrics.dagger.UdfpsModule;
import com.android.systemui.bouncer.ui.BouncerViewModule;
import com.android.systemui.classifier.FalsingModule;
import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule;
import com.android.systemui.common.ui.data.repository.CommonRepositoryModule;
@@ -52,7 +53,6 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.FlagsModule;
import com.android.systemui.keyboard.KeyboardModule;
import com.android.systemui.bouncer.ui.BouncerViewModule;
import com.android.systemui.log.dagger.LogModule;
import com.android.systemui.log.dagger.MonitorLog;
import com.android.systemui.log.table.TableLogBuffer;
@@ -73,6 +73,7 @@ import com.android.systemui.qs.QSFragmentStartableModule;
import com.android.systemui.qs.footer.dagger.FooterActionsModule;
import com.android.systemui.recents.Recents;
import com.android.systemui.retail.dagger.RetailModeModule;
import com.android.systemui.scene.SceneContainerFrameworkModule;
import com.android.systemui.screenrecord.ScreenRecordModule;
import com.android.systemui.screenshot.dagger.ScreenshotModule;
import com.android.systemui.security.data.repository.SecurityRepositoryModule;
@@ -185,6 +186,7 @@ import javax.inject.Named;
QRCodeScannerModule.class,
QSFragmentStartableModule.class,
RetailModeModule.class,
SceneContainerFrameworkModule.class,
ScreenshotModule.class,
SensorModule.class,
SecurityRepositoryModule.class,

View File

@@ -16,7 +16,7 @@
package com.android.systemui.scene
import com.android.systemui.scene.data.model.SceneContainerConfigModule
import com.android.systemui.scene.shared.model.SceneContainerConfigModule
import com.android.systemui.scene.ui.composable.SceneModule
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModelModule
import dagger.Module

View File

@@ -16,7 +16,7 @@
package com.android.systemui.scene.data.repository
import com.android.systemui.scene.data.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import javax.inject.Inject
@@ -28,11 +28,9 @@ import kotlinx.coroutines.flow.asStateFlow
class SceneContainerRepository
@Inject
constructor(
containerConfigurations: Set<SceneContainerConfig>,
private val containerConfigByName: Map<String, SceneContainerConfig>,
) {
private val containerConfigByName: Map<String, SceneContainerConfig> =
containerConfigurations.associateBy { config -> config.name }
private val containerVisibilityByName: Map<String, MutableStateFlow<Boolean>> =
containerConfigByName
.map { (containerName, _) -> containerName to MutableStateFlow(true) }
@@ -48,21 +46,6 @@ constructor(
.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.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 The Android Open Source Project
* 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.
@@ -14,9 +14,7 @@
* limitations under the License.
*/
package com.android.systemui.scene.data.model
import com.android.systemui.scene.shared.model.SceneKey
package com.android.systemui.scene.shared.model
/** Models the configuration of a single scene container. */
data class SceneContainerConfig(

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023 The Android Open Source Project
* 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.
@@ -14,11 +14,9 @@
* limitations under the License.
*/
package com.android.systemui.scene.data.model
package com.android.systemui.scene.shared.model
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import dagger.Module
import dagger.Provides
import javax.inject.Named

View File

@@ -0,0 +1,60 @@
/*
* 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.startable
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.ui.view.SceneWindowRootView
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import dagger.Binds
import dagger.Module
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Inject
import javax.inject.Named
@SysUISingleton
class SceneContainerStartable
@Inject
constructor(
private val view: WindowRootView,
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT) private val viewModel: SceneContainerViewModel,
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT) private val containerConfig: SceneContainerConfig,
@Named(SceneContainerNames.SYSTEM_UI_DEFAULT)
private val scenes: Set<@JvmSuppressWildcards Scene>,
) : CoreStartable {
override fun start() {
(view as? SceneWindowRootView)?.init(
viewModel = viewModel,
containerConfig = containerConfig,
scenes = scenes,
)
}
}
@Module
interface SceneContainerStartableModule {
@Binds
@IntoMap
@ClassKey(SceneContainerStartable::class)
fun bind(impl: SceneContainerStartable): CoreStartable
}

View File

@@ -2,6 +2,73 @@ 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 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(context: Context?, attrs: AttributeSet?) : WindowRootView(context, attrs)
class SceneWindowRootView(
context: Context,
attrs: AttributeSet?,
) :
WindowRootView(
context,
attrs,
) {
fun init(
viewModel: SceneContainerViewModel,
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)
}
}
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()
}
}
}
}

View File

@@ -2,7 +2,37 @@ package com.android.systemui.scene.ui.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import com.android.systemui.compose.ComposeFacade
/** A view that can serve as the root of the main SysUI window. */
open class WindowRootView(context: Context?, attrs: AttributeSet?) : FrameLayout(context, attrs)
/** A view that can serve as the root of the main SysUI window. */
open class WindowRootView(
context: Context,
attrs: AttributeSet?,
) :
FrameLayout(
context,
attrs,
) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (ComposeFacade.isComposeAvailable() && isRoot()) {
ComposeFacade.composeInitializer().onAttachedToWindow(this)
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
if (ComposeFacade.isComposeAvailable() && isRoot()) {
ComposeFacade.composeInitializer().onDetachedFromWindow(this)
}
}
private fun isRoot(): Boolean {
return parent.let { it !is View || it.id == android.R.id.content }
}
}

View File

@@ -58,7 +58,6 @@ import android.widget.FrameLayout;
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.R;
import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.scene.ui.view.WindowRootView;
/**
@@ -149,18 +148,6 @@ public class NotificationShadeWindowView extends WindowRootView {
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setWillNotDraw(!DEBUG);
if (ComposeFacade.INSTANCE.isComposeAvailable()) {
ComposeFacade.INSTANCE.composeInitializer().onAttachedToWindow(this);
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (ComposeFacade.INSTANCE.isComposeAvailable()) {
ComposeFacade.INSTANCE.composeInitializer().onDetachedFromWindow(this);
}
}
@Override

View File

@@ -16,6 +16,7 @@
package com.android.systemui.shade
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.os.Handler
import android.view.LayoutInflater
@@ -28,6 +29,7 @@ import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.biometrics.AuthRippleController
import com.android.systemui.biometrics.AuthRippleView
import com.android.systemui.compose.ComposeFacade
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
@@ -62,13 +64,16 @@ abstract class ShadeModule {
companion object {
const val SHADE_HEADER = "large_screen_shade_header"
@SuppressLint("InflateParams") // Root views don't have parents.
@Provides
@SysUISingleton
fun providesWindowRootView(
layoutInflater: LayoutInflater,
featureFlags: FeatureFlags,
): WindowRootView {
return if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
return if (
featureFlags.isEnabled(Flags.SCENE_CONTAINER) && ComposeFacade.isComposeAvailable()
) {
layoutInflater.inflate(R.layout.scene_window_root, null)
} else {
layoutInflater.inflate(R.layout.super_notification_shade, null)

View File

@@ -24,9 +24,9 @@ import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
import com.android.systemui.scene.data.model.SceneContainerConfig
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,7 +51,7 @@ class SceneTestUtils(
fakeSceneContainerConfig(CONTAINER_2),
)
): SceneContainerRepository {
return SceneContainerRepository(containerConfigurations)
return SceneContainerRepository(containerConfigurations.associateBy { it.name })
}
fun fakeSceneKeys(): List<SceneKey> {