From be4a1a291185f55f1e0525de3e68215ae891f989 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Fri, 6 Jan 2023 12:47:02 +0100 Subject: [PATCH 1/4] Add a Microbenchmark for the PeopleSpace activity (2/2) Bug: 238993727 Test: PeopleSpaceActivityTest Change-Id: I1fcf58875f8f07c9b810870944717ae7eab31724 --- .../compose/modifiers/SysuiTestTag.kt | 32 +++++++++++++++++++ .../people/ui/compose/PeopleScreen.kt | 5 ++- .../systemui/people/PeopleSpaceActivity.java | 2 ++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/SysuiTestTag.kt 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 0000000000000..9eb78e14ab4ed --- /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 23dacf9946f33..3d7be7735a34f 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, onTileClicked: (PeopleTileViewModel) -> Unit, ) { - Column { + Column( + Modifier.sysuiResTag("top_level_with_conversations"), + ) { Column( Modifier.fillMaxWidth().padding(PeopleSpacePadding), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceActivity.java index fba5f63ea9c78..7f0f894152808 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); From 6bc0686d17164763938f2bca5487b0e9ea9f7856 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Fri, 6 Jan 2023 16:28:24 +0100 Subject: [PATCH 2/4] Add a Microbenchmark for the conversations list (2/2) Bug: 238993727 Test: PeopleSpaceFlingTest Change-Id: I0d801554e4e6da3d5c87b4f7c71afe457c2d4035 --- .../src/com/android/systemui/people/ui/compose/PeopleScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3d7be7735a34f..3eeadae5385f8 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 @@ -135,7 +135,7 @@ private fun PeopleScreenWithConversations( } LazyColumn( - Modifier.fillMaxWidth(), + Modifier.fillMaxWidth().sysuiResTag("scroll_view"), contentPadding = PaddingValues( top = 16.dp, From 54a0fae8580f667b84fc0c058fdd126aaef3aea8 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Wed, 4 Jan 2023 11:36:46 +0100 Subject: [PATCH 3/4] 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 --- packages/SystemUI/Android.bp | 1 + .../android/systemui/compose/ComposeFacade.kt | 6 +- .../android/systemui/compose/ComposeFacade.kt | 2 + .../compose/ComposeInitializerImpl.kt | 78 +++++++++++++++++++ .../systemui/compose/BaseComposeFacade.kt | 5 ++ .../systemui/compose/ComposeInitializer.kt | 48 ++++++++++++ .../systemui/lifecycle/RepeatWhenAttached.kt | 2 +- .../shade/NotificationShadeWindowView.java | 13 ++++ .../compose/ComposeInitializerTest.kt | 66 ++++++++++++++++ 9 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeInitializerImpl.kt create mode 100644 packages/SystemUI/src/com/android/systemui/compose/ComposeInitializer.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/compose/ComposeInitializerTest.kt 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) + } + } +} From 99bda8cc88011f01cc4992de021848c2f4e1ca83 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Fri, 30 Dec 2022 11:33:28 +0100 Subject: [PATCH 4/4] Show the Compose implementation of the footer actions when available This CLs shows the Compose implementation of the QS footer actions when Compose is enabled (it is not by default). Bug: 242040009 Test: Build with SYSTEMUI_USE_COMPOSE=true and use the footer actions buttons. Change-Id: I41a2a65fdf108849a8b2a7d381dab9d7d644f89a --- .../android/systemui/compose/ComposeFacade.kt | 12 +++++++ .../android/systemui/compose/ComposeFacade.kt | 16 ++++++++++ .../qs/footer/ui/compose/FooterActions.kt | 7 ++-- .../systemui/compose/BaseComposeFacade.kt | 11 +++++++ .../com/android/systemui/qs/QSFragment.java | 32 +++++++++++++++++-- 5 files changed, 72 insertions(+), 6 deletions(-) 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 89b2d83657907..e253fb925ceb1 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,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 /** The Compose facade, when Compose is *not* available. */ object ComposeFacade : BaseComposeFacade { @@ -36,6 +40,14 @@ object ComposeFacade : BaseComposeFacade { 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" + 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 1c2000f04a813..1ea18fec4abe7 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,11 +16,17 @@ 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 { @@ -35,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/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 5c5ceefbd6fb6..349f5c3331165 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 3080beec718be..c0f854958c419 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. @@ -46,4 +50,11 @@ interface BaseComposeFacade { 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/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index f8fb4e89111e1..4ad23136a3adc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -47,6 +47,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; @@ -225,9 +226,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); @@ -288,6 +287,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;