diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index e148d4f547a43..2529157035112 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -271,6 +271,7 @@ android_library { "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 6e728ce7248f1..89b2d83657907 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 @@ import com.android.systemui.people.ui.viewmodel.PeopleViewModel object ComposeFacade : BaseComposeFacade { override fun isComposeAvailable(): Boolean = false + override fun composeInitializer(): ComposeInitializer { + throwComposeUnavailableError() + } + override fun setPeopleSpaceActivityContent( activity: ComponentActivity, viewModel: PeopleViewModel, @@ -32,7 +36,7 @@ object ComposeFacade : BaseComposeFacade { 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 6991ff82c2d19..1c2000f04a813 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 @@ import com.android.systemui.people.ui.viewmodel.PeopleViewModel 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 0000000000000..772c8918fd2d3 --- /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 e5ec727f04379..3080beec718be 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 @@ interface BaseComposeFacade { */ 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 0000000000000..90dc3a00daa25 --- /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 e3649187b0a74..d69ac7fe035d0 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 fun createLifecycleOwnerAndRun( * └───────────────┴───────────────────┴──────────────┴─────────────────┘ * ``` */ -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 6acf417f0ea64..1f0cbf9af51c8 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 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; /** * Combined keyguard and notification panel view. Also holding backdrop and scrims. @@ -149,6 +150,18 @@ public class NotificationShadeWindowView extends FrameLayout { 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 0000000000000..3e6cc3bb4f6bc --- /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) + } + } +}