summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/Android.bp1
-rw-r--r--packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt18
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt18
-rw-r--r--packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt78
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt32
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt7
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt16
-rw-r--r--packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt48
-rw-r--r--packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java2
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/QSFragment.java32
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt66
14 files changed, 330 insertions, 10 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index e148d4f547a4..252915703511 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 6e728ce7248f..e253fb925ceb 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
@@ -17,13 +17,21 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
/** The Compose facade, when Compose is *not* available. */
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = false
+ override fun composeInitializer(): ComposeInitializer {
+ throwComposeUnavailableError()
+ }
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
@@ -32,7 +40,15 @@ object ComposeFacade : BaseComposeFacade {
throwComposeUnavailableError()
}
- private fun throwComposeUnavailableError() {
+ override fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner
+ ): View {
+ 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 6991ff82c2d1..1ea18fec4abe 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
@@ -16,16 +16,24 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.ui.platform.ComposeView
+import androidx.lifecycle.LifecycleOwner
import com.android.compose.theme.PlatformTheme
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
/** The Compose facade, when Compose is available. */
object ComposeFacade : BaseComposeFacade {
override fun isComposeAvailable(): Boolean = true
+ override fun composeInitializer(): ComposeInitializer = ComposeInitializerImpl
+
override fun setPeopleSpaceActivityContent(
activity: ComponentActivity,
viewModel: PeopleViewModel,
@@ -33,4 +41,14 @@ object ComposeFacade : BaseComposeFacade {
) {
activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } }
}
+
+ override fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner,
+ ): View {
+ return ComposeView(context).apply {
+ setContent { PlatformTheme { FooterActions(viewModel, qsVisibilityLifecycleOwner) } }
+ }
+ }
}
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 000000000000..772c8918fd2d
--- /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/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
new file mode 100644
index 000000000000..9eb78e14ab4e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.modifiers
+
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.semantics.testTagsAsResourceId
+
+/**
+ * Set a test tag on this node so that it is associated with [resId]. This node will then be
+ * accessible by integration tests using `sysuiResSelector(resId)`.
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.sysuiResTag(resId: String): Modifier {
+ return this.semantics { testTagsAsResourceId = true }.testTag("com.android.systemui:id/$resId")
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
index 23dacf9946f3..3eeadae5385f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -51,6 +51,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.R
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
@@ -110,7 +111,9 @@ private fun PeopleScreenWithConversations(
recentTiles: List<PeopleTileViewModel>,
onTileClicked: (PeopleTileViewModel) -> Unit,
) {
- Column {
+ Column(
+ Modifier.sysuiResTag("top_level_with_conversations"),
+ ) {
Column(
Modifier.fillMaxWidth().padding(PeopleSpacePadding),
horizontalAlignment = Alignment.CenterHorizontally,
@@ -132,7 +135,7 @@ private fun PeopleScreenWithConversations(
}
LazyColumn(
- Modifier.fillMaxWidth(),
+ Modifier.fillMaxWidth().sysuiResTag("scroll_view"),
contentPadding =
PaddingValues(
top = 16.dp,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
index 5c5ceefbd6fb..349f5c333116 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt
@@ -73,6 +73,7 @@ import com.android.systemui.R
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel
@@ -180,9 +181,9 @@ fun FooterActions(
security?.let { SecurityButton(it, Modifier.weight(1f)) }
foregroundServices?.let { ForegroundServicesButton(it) }
- userSwitcher?.let { IconButton(it) }
- IconButton(viewModel.settings)
- viewModel.power?.let { IconButton(it) }
+ userSwitcher?.let { IconButton(it, Modifier.sysuiResTag("multi_user_switch")) }
+ IconButton(viewModel.settings, Modifier.sysuiResTag("settings_button_container"))
+ viewModel.power?.let { IconButton(it, Modifier.sysuiResTag("pm_lite")) }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index e5ec727f0437..c0f854958c41 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,8 +17,12 @@
package com.android.systemui.compose
+import android.content.Context
+import android.view.View
import androidx.activity.ComponentActivity
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
/**
* A facade to interact with Compose, when it is available.
@@ -35,10 +39,22 @@ 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,
viewModel: PeopleViewModel,
onResult: (PeopleViewModel.Result) -> Unit,
)
+
+ /** Create a [View] to represent [viewModel] on screen. */
+ fun createFooterActionsView(
+ context: Context,
+ viewModel: FooterActionsViewModel,
+ qsVisibilityLifecycleOwner: LifecycleOwner,
+ ): View
}
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 000000000000..90dc3a00daa2
--- /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 e3649187b0a7..d69ac7fe035d 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/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
index fba5f63ea9c7..7f0f89415280 100644
--- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java
@@ -68,8 +68,10 @@ public class PeopleSpaceActivity extends ComponentActivity {
};
if (ComposeFacade.INSTANCE.isComposeAvailable()) {
+ Log.d(TAG, "Using the Compose implementation of the PeopleSpaceActivity");
ComposeFacade.INSTANCE.setPeopleSpaceActivityContent(this, viewModel, onResult);
} else {
+ Log.d(TAG, "Using the View implementation of the PeopleSpaceActivity");
ViewGroup view = PeopleViewBinder.create(this);
PeopleViewBinder.bind(view, viewModel, /* lifecycleOwner= */ this, onResult);
setContentView(view);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 774cb3442811..8ad102ece9b3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -49,6 +49,7 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.animation.ShadeInterpolation;
+import com.android.systemui.compose.ComposeFacade;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.ui.MediaHost;
@@ -228,9 +229,7 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
mQSFooterActionsViewModel = mFooterActionsViewModelFactory.create(/* lifecycleOwner */
this);
- LinearLayout footerActionsView = view.findViewById(R.id.qs_footer_actions);
- FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
- mListeningAndVisibilityLifecycleOwner);
+ bindFooterActionsView(view);
mFooterActionsController.init();
mQSPanelScrollView = view.findViewById(R.id.expanded_qs_scroll_view);
@@ -291,6 +290,33 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
});
}
+ private void bindFooterActionsView(View root) {
+ LinearLayout footerActionsView = root.findViewById(R.id.qs_footer_actions);
+
+ if (!ComposeFacade.INSTANCE.isComposeAvailable()) {
+ Log.d(TAG, "Binding the View implementation of the QS footer actions");
+ FooterActionsViewBinder.bind(footerActionsView, mQSFooterActionsViewModel,
+ mListeningAndVisibilityLifecycleOwner);
+ return;
+ }
+
+ // Compose is available, so let's use the Compose implementation of the footer actions.
+ Log.d(TAG, "Binding the Compose implementation of the QS footer actions");
+ View composeView = ComposeFacade.INSTANCE.createFooterActionsView(root.getContext(),
+ mQSFooterActionsViewModel, mListeningAndVisibilityLifecycleOwner);
+
+ // The id R.id.qs_footer_actions is used by QSContainerImpl to set the horizontal margin
+ // to all views except for qs_footer_actions, so we set it to the Compose view.
+ composeView.setId(R.id.qs_footer_actions);
+
+ // Replace the View by the Compose provided one.
+ ViewGroup parent = (ViewGroup) footerActionsView.getParent();
+ ViewGroup.LayoutParams layoutParams = footerActionsView.getLayoutParams();
+ int index = parent.indexOfChild(footerActionsView);
+ parent.removeViewAt(index);
+ parent.addView(composeView, index, layoutParams);
+ }
+
@Override
public void setScrollListener(ScrollListener listener) {
mScrollListener = listener;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java
index 6acf417f0ea6..1f0cbf9af51c 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 000000000000..3e6cc3bb4f6b
--- /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)
+ }
+ }
+}