Introduce ComposeInitializer (1/2)
This CL introduces ComposeInitializer, which can be used by Window root
views to make Compose usable in that window. Once a Window is
initialized, we can simply add ComposeViews to the Window hierarchy and
they will work as expected.
This CL also uses ComposeInitializer in the NotificationShadeWindowView.
This will be used by the Quick Settings footer actions, that have
already been implemented using Compose.
Bug: 230740991
Test: ComposeInitializerTest
Change-Id: I232bd12d3a46083101663a5bc557472263c27c38
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e148d4f..2529157 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -271,6 +271,7 @@
"LowLightDreamLib",
"motion_tool_lib",
"androidx.core_core-animation-testing-nodeps",
+ "androidx.compose.ui_ui",
],
}
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6e728ce..89b2d83 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -24,6 +24,10 @@
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = false
+ override fun composeInitializer(): ComposeInitializer {
+ throwComposeUnavailableError()
+ }
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
@@ -32,7 +36,7 @@
throwComposeUnavailableError()
}
- private fun throwComposeUnavailableError() {
+ private fun throwComposeUnavailableError(): Nothing {
error(
"Compose is not available. Make sure to check isComposeAvailable() before calling any" +
" other function on ComposeFacade."
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index 6991ff8..1c2000f 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -26,6 +26,8 @@
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = true
+ override fun composeInitializer(): ComposeInitializer = ComposeInitializerImpl
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
new file mode 100644
index 0000000..772c891
--- /dev/null
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 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.compose
+
+import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.ViewTreeLifecycleOwner
+import androidx.savedstate.SavedStateRegistry
+import androidx.savedstate.SavedStateRegistryController
+import androidx.savedstate.SavedStateRegistryOwner
+import com.android.compose.animation.ViewTreeSavedStateRegistryOwner
+import com.android.systemui.lifecycle.ViewLifecycleOwner
+
+internal object ComposeInitializerImpl : ComposeInitializer {
+ override fun onAttachedToWindow(root: View) {
+ if (ViewTreeLifecycleOwner.get(root) != null) {
+ error("root $root already has a LifecycleOwner")
+ }
+
+ val parent = root.parent
+ if (parent is View && parent.id != android.R.id.content) {
+ error(
+ "ComposeInitializer.onAttachedToWindow(View) must be called on the content child." +
+ "Outside of activities and dialogs, this is usually the top-most View of a " +
+ "window."
+ )
+ }
+
+ // The lifecycle owner, which is STARTED when [root] is visible and RESUMED when [root] is
+ // both visible and focused.
+ val lifecycleOwner = ViewLifecycleOwner(root)
+
+ // We create a trivial implementation of [SavedStateRegistryOwner] that does not do any save
+ // or restore because SystemUI process is always running and top-level windows using this
+ // initializer are created once, when the process is started.
+ val savedStateRegistryOwner =
+ object : SavedStateRegistryOwner {
+ private val savedStateRegistry =
+ SavedStateRegistryController.create(this).apply { performRestore(null) }
+
+ override fun getLifecycle(): Lifecycle = lifecycleOwner.lifecycle
+
+ override fun getSavedStateRegistry(): SavedStateRegistry {
+ return savedStateRegistry.savedStateRegistry
+ }
+ }
+
+ // We must call [ViewLifecycleOwner.onCreate] after creating the [SavedStateRegistryOwner]
+ // because `onCreate` might move the lifecycle state to STARTED which will make
+ // [SavedStateRegistryController.performRestore] throw.
+ lifecycleOwner.onCreate()
+
+ // Set the owners on the root. They will be reused by any ComposeView inside the root
+ // hierarchy.
+ ViewTreeLifecycleOwner.set(root, lifecycleOwner)
+ ViewTreeSavedStateRegistryOwner.set(root, savedStateRegistryOwner)
+ }
+
+ override fun onDetachedFromWindow(root: View) {
+ (ViewTreeLifecycleOwner.get(root) as ViewLifecycleOwner).onDestroy()
+ ViewTreeLifecycleOwner.set(root, null)
+ ViewTreeSavedStateRegistryOwner.set(root, null)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index e5ec727..3080bee 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -35,6 +35,11 @@
*/
fun isComposeAvailable(): Boolean
+ /**
+ * Return the [ComposeInitializer] to make Compose usable in windows outside normal activities.
+ */
+ fun composeInitializer(): ComposeInitializer
+
/** Bind the content of [activity] to [viewModel]. */
fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
new file mode 100644
index 0000000..90dc3a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import android.view.View
+
+/**
+ * An initializer to use Compose outside of an Activity, e.g. inside a window added directly using
+ * [android.view.WindowManager.addView] (like the shade or status bar) or inside a dialog.
+ *
+ * Example:
+ * ```
+ * windowManager.addView(MyWindowRootView(context), /* layoutParams */)
+ *
+ * class MyWindowRootView(context: Context) : FrameLayout(context) {
+ * override fun onAttachedToWindow() {
+ * super.onAttachedToWindow()
+ * ComposeInitializer.onAttachedToWindow(this)
+ * }
+ *
+ * override fun onDetachedFromWindow() {
+ * super.onDetachedFromWindow()
+ * ComposeInitializer.onDetachedFromWindow(this)
+ * }
+ * }
+ * ```
+ */
+interface ComposeInitializer {
+ /** Function to be called on your window root view's [View.onAttachedToWindow] function. */
+ fun onAttachedToWindow(root: View)
+
+ /** Function to be called on your window root view's [View.onDetachedFromWindow] function. */
+ fun onDetachedFromWindow(root: View)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index e364918..d69ac7f 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -145,7 +145,7 @@
* └───────────────┴───────────────────┴──────────────┴─────────────────┘
* ```
*/
-private class ViewLifecycleOwner(
+class ViewLifecycleOwner(
private val view: View,
) : LifecycleOwner {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 6acf417..1f0cbf9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
@@ -58,6 +58,7 @@
import com.android.internal.view.FloatingActionMode;
import com.android.internal.widget.floatingtoolbar.FloatingToolbar;
import com.android.systemui.R;
+import com.android.systemui.compose.ComposeFacade;
/**
* Combined keyguard and notification panel view. Also holding backdrop and scrims.
@@ -149,6 +150,18 @@
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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
new file mode 100644
index 0000000..3e6cc3b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 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.compose
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import android.testing.ViewUtils
+import android.widget.FrameLayout
+import androidx.compose.ui.platform.ComposeView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ComposeInitializerTest : SysuiTestCase() {
+ @Test
+ fun testCanAddComposeViewInInitializedWindow() {
+ if (!ComposeFacade.isComposeAvailable()) {
+ return
+ }
+
+ val root = TestWindowRoot(context)
+ try {
+ runOnMainThreadAndWaitForIdleSync { ViewUtils.attachView(root) }
+ assertThat(root.isAttachedToWindow).isTrue()
+
+ runOnMainThreadAndWaitForIdleSync { root.addView(ComposeView(context)) }
+ } finally {
+ runOnMainThreadAndWaitForIdleSync { ViewUtils.detachView(root) }
+ }
+ }
+
+ private fun runOnMainThreadAndWaitForIdleSync(f: () -> Unit) {
+ mContext.mainExecutor.execute(f)
+ waitForIdleSync()
+ }
+
+ class TestWindowRoot(context: Context) : FrameLayout(context) {
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ ComposeFacade.composeInitializer().onAttachedToWindow(this)
+ }
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ ComposeFacade.composeInitializer().onDetachedFromWindow(this)
+ }
+ }
+}