Merge changes from topics "people-space-benchmark", "people-space-jank-benchmark", "sysui-compose-initializer" into tm-qpr-dev

* changes:
  Show the Compose implementation of the footer actions when available
  Introduce ComposeInitializer (1/2)
  Add a Microbenchmark for the conversations list (2/2)
  Add a Microbenchmark for the PeopleSpace activity (2/2)
This commit is contained in:
Jordan Demeulenaere
2023-01-13 11:29:28 +00:00
committed by Android (Google) Code Review
14 changed files with 330 additions and 10 deletions

View File

@@ -271,6 +271,7 @@ android_library {
"LowLightDreamLib",
"motion_tool_lib",
"androidx.core_core-animation-testing-nodeps",
"androidx.compose.ui_ui",
],
}

View File

@@ -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."

View File

@@ -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) } }
}
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}

View File

@@ -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,

View File

@@ -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")) }
}
}
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -145,7 +145,7 @@ private fun createLifecycleOwnerAndRun(
* └───────────────┴───────────────────┴──────────────┴─────────────────┘
* ```
*/
private class ViewLifecycleOwner(
class ViewLifecycleOwner(
private val view: View,
) : LifecycleOwner {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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)
}
}
}