Implement the People UI in Compose (1/2)
This CL implements the People UI using Jetpack Compose. Note that this
implementation won't be used by SystemUI yet, and can only be viewed
using the SystemUIGallery app at the moment.
Once ag/19500380 is submitted (blocked by b/236146570), the screenshot
tests will use the Compose implementation. You can see the visual
difference between the implementations in ag/19498514. As explained in
b/238993727#comment10 the result is slightly different (and improved
over the current View implementation).
Note that the fake values of this screen have been moved from the
current PeopleSpaceScreenshotTest, and are now reused there as well (see
ag/19568794).
Bug: 238993727
Test: atest SystemUIGoogleScreenshotTests
Change-Id: I4d2f241ccf768ef3fccc01ce196418ce7fc26492
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp
index 40218de..325ede6 100644
--- a/packages/SystemUI/compose/features/Android.bp
+++ b/packages/SystemUI/compose/features/Android.bp
@@ -30,6 +30,7 @@
],
static_libs: [
+ "SystemUI-core",
"SystemUIComposeCore",
"androidx.compose.runtime_runtime",
diff --git a/packages/SystemUI/compose/features/AndroidManifest.xml b/packages/SystemUI/compose/features/AndroidManifest.xml
index 0aea99d..eada40e 100644
--- a/packages/SystemUI/compose/features/AndroidManifest.xml
+++ b/packages/SystemUI/compose/features/AndroidManifest.xml
@@ -16,7 +16,38 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+
package="com.android.systemui.compose.features">
-
-
+ <application
+ android:name="android.app.Application"
+ android:appComponentFactory="androidx.core.app.AppComponentFactory"
+ tools:replace="android:name,android:appComponentFactory">
+ <!-- Disable providers from SystemUI -->
+ <provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
+ android:authorities="com.android.systemui.test.keyguard.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
+ android:authorities="com.android.systemui.test.keyguard.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
+ android:authorities="com.android.systemui.test.keyguard.clock.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="com.android.systemui.people.PeopleProvider"
+ android:authorities="com.android.systemui.test.people.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+ <provider android:name="androidx.core.content.FileProvider"
+ android:authorities="com.android.systemui.test.fileprovider.disabled"
+ android:enabled="false"
+ tools:replace="android:authorities"
+ tools:node="remove"/>
+ </application>
</manifest>
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
new file mode 100644
index 0000000..2bf1937
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt
@@ -0,0 +1,233 @@
+/*
+ * 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.people.ui.compose
+
+import android.annotation.StringRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.people.ui.viewmodel.PeopleTileViewModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import kotlinx.coroutines.flow.collect
+
+/**
+ * Compose the screen associated to a [PeopleViewModel].
+ *
+ * @param viewModel the [PeopleViewModel] that should be composed.
+ * @param onResult the callback called with the result of this screen. Callers should usually finish
+ * the Activity/Fragment/View hosting this Composable once a result is available.
+ */
+@Composable
+fun PeopleScreen(
+ viewModel: PeopleViewModel,
+ onResult: (PeopleViewModel.Result) -> Unit,
+) {
+ val priorityTiles by viewModel.priorityTiles.collectAsState()
+ val recentTiles by viewModel.recentTiles.collectAsState()
+
+ // Make sure to refresh the tiles/conversations when the lifecycle is resumed, so that it
+ // updates them when going back to the Activity after leaving it.
+ val lifecycleOwner = LocalLifecycleOwner.current
+ LaunchedEffect(lifecycleOwner, viewModel) {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
+ viewModel.onTileRefreshRequested()
+ }
+ }
+
+ // Call [onResult] this activity when the ViewModel tells us so.
+ LaunchedEffect(viewModel.result) {
+ viewModel.result.collect { result ->
+ if (result != null) {
+ viewModel.clearResult()
+ onResult(result)
+ }
+ }
+ }
+
+ // Make sure to use the Android colors and not the default Material3 colors to have the exact
+ // same colors as the View implementation.
+ val androidColors = LocalAndroidColorScheme.current
+ Surface(
+ color = androidColors.colorBackground,
+ contentColor = androidColors.textColorPrimary,
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ if (priorityTiles.isNotEmpty() || recentTiles.isNotEmpty()) {
+ PeopleScreenWithConversations(priorityTiles, recentTiles, viewModel::onTileClicked)
+ } else {
+ PeopleScreenEmpty(viewModel::onUserJourneyCancelled)
+ }
+ }
+}
+
+@Composable
+private fun PeopleScreenWithConversations(
+ priorityTiles: List<PeopleTileViewModel>,
+ recentTiles: List<PeopleTileViewModel>,
+ onTileClicked: (PeopleTileViewModel) -> Unit,
+) {
+ Column {
+ Column(
+ Modifier.fillMaxWidth().padding(PeopleSpacePadding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ stringResource(R.string.select_conversation_title),
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ )
+
+ Spacer(Modifier.height(24.dp))
+
+ Text(
+ stringResource(R.string.select_conversation_text),
+ Modifier.padding(horizontal = 24.dp),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ )
+ }
+
+ LazyColumn(
+ Modifier.fillMaxWidth(),
+ contentPadding =
+ PaddingValues(
+ top = 16.dp,
+ bottom = PeopleSpacePadding,
+ start = 8.dp,
+ end = 8.dp,
+ )
+ ) {
+ ConversationList(R.string.priority_conversations, priorityTiles, onTileClicked)
+ item { Spacer(Modifier.height(35.dp)) }
+ ConversationList(R.string.recent_conversations, recentTiles, onTileClicked)
+ }
+ }
+}
+
+private fun LazyListScope.ConversationList(
+ @StringRes headerTextResource: Int,
+ tiles: List<PeopleTileViewModel>,
+ onTileClicked: (PeopleTileViewModel) -> Unit
+) {
+ item {
+ Text(
+ stringResource(headerTextResource),
+ Modifier.padding(start = 16.dp),
+ style = MaterialTheme.typography.labelLarge,
+ color = LocalAndroidColorScheme.current.colorAccentPrimaryVariant,
+ )
+
+ Spacer(Modifier.height(10.dp))
+ }
+
+ tiles.forEachIndexed { index, tile ->
+ if (index > 0) {
+ item {
+ Divider(
+ color = LocalAndroidColorScheme.current.colorBackground,
+ thickness = 2.dp,
+ )
+ }
+ }
+
+ item(tile.key.toString()) {
+ Tile(
+ tile,
+ onTileClicked,
+ withTopCornerRadius = index == 0,
+ withBottomCornerRadius = index == tiles.lastIndex,
+ )
+ }
+ }
+}
+
+@Composable
+private fun Tile(
+ tile: PeopleTileViewModel,
+ onTileClicked: (PeopleTileViewModel) -> Unit,
+ withTopCornerRadius: Boolean,
+ withBottomCornerRadius: Boolean,
+) {
+ val androidColors = LocalAndroidColorScheme.current
+ val cornerRadius = dimensionResource(R.dimen.people_space_widget_radius)
+ val topCornerRadius = if (withTopCornerRadius) cornerRadius else 0.dp
+ val bottomCornerRadius = if (withBottomCornerRadius) cornerRadius else 0.dp
+
+ Surface(
+ color = androidColors.colorSurface,
+ contentColor = androidColors.textColorPrimary,
+ shape =
+ RoundedCornerShape(
+ topStart = topCornerRadius,
+ topEnd = topCornerRadius,
+ bottomStart = bottomCornerRadius,
+ bottomEnd = bottomCornerRadius,
+ ),
+ ) {
+ Row(
+ Modifier.fillMaxWidth().clickable { onTileClicked(tile) }.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Image(
+ tile.icon.asImageBitmap(),
+ // TODO(b/238993727): Add a content description.
+ contentDescription = null,
+ Modifier.size(dimensionResource(R.dimen.avatar_size_for_medium)),
+ )
+
+ Text(
+ tile.username ?: "",
+ Modifier.padding(horizontal = 16.dp),
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+ }
+}
+
+/** The padding applied to the PeopleSpace screen. */
+internal val PeopleSpacePadding = 24.dp
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
new file mode 100644
index 0000000..5c9358f
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt
@@ -0,0 +1,124 @@
+/*
+ * 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.people.ui.compose
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.android.systemui.R
+import com.android.systemui.compose.theme.LocalAndroidColorScheme
+
+@Composable
+internal fun PeopleScreenEmpty(
+ onGotItClicked: () -> Unit,
+) {
+ Column(
+ Modifier.fillMaxSize().padding(PeopleSpacePadding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ stringResource(R.string.select_conversation_title),
+ style = MaterialTheme.typography.headlineSmall,
+ textAlign = TextAlign.Center,
+ )
+
+ Spacer(Modifier.height(50.dp))
+
+ Text(
+ stringResource(R.string.no_conversations_text),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ )
+
+ Spacer(Modifier.weight(1f))
+ ExampleTile()
+ Spacer(Modifier.weight(1f))
+
+ val androidColors = LocalAndroidColorScheme.current
+ Button(
+ onGotItClicked,
+ Modifier.fillMaxWidth().defaultMinSize(minHeight = 56.dp),
+ colors =
+ ButtonDefaults.buttonColors(
+ containerColor = androidColors.colorAccentPrimary,
+ contentColor = androidColors.textColorOnAccent,
+ )
+ ) { Text(stringResource(R.string.got_it)) }
+ }
+}
+
+@Composable
+private fun ExampleTile() {
+ val androidColors = LocalAndroidColorScheme.current
+ Surface(
+ shape = RoundedCornerShape(28.dp),
+ color = androidColors.colorSurface,
+ contentColor = androidColors.textColorPrimary,
+ ) {
+ Row(
+ Modifier.padding(vertical = 20.dp, horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ // TODO(b/238993727): Add a content description.
+ Image(
+ painterResource(R.drawable.ic_avatar_with_badge),
+ contentDescription = null,
+ Modifier.size(40.dp),
+ )
+ Spacer(Modifier.height(2.dp))
+ Text(
+ stringResource(R.string.empty_user_name),
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ Spacer(Modifier.width(24.dp))
+
+ Text(
+ stringResource(R.string.empty_status),
+ style = MaterialTheme.typography.labelMedium,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp
index 40504dc..b0f5cc1 100644
--- a/packages/SystemUI/compose/gallery/Android.bp
+++ b/packages/SystemUI/compose/gallery/Android.bp
@@ -27,6 +27,7 @@
srcs: [
"src/**/*.kt",
+ ":SystemUI-tests-utils",
],
resource_dirs: [
@@ -45,6 +46,14 @@
"androidx.navigation_navigation-compose",
"androidx.appcompat_appcompat",
+
+ // TODO(b/240431193): Remove the dependencies and depend on
+ // SystemUI-test-utils directly.
+ "androidx.test.runner",
+ "mockito-target-extended-minus-junit4",
+ "testables",
+ "truth-prebuilt",
+ "androidx.test.uiautomator",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
index c341867..bb98fb3 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
@@ -13,7 +13,7 @@
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -33,6 +33,28 @@
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
+ val PeopleEmpty =
+ ChildScreen("people_empty") { navController ->
+ EmptyPeopleScreen(onResult = { navController.popBackStack() })
+ }
+ val PeopleFew =
+ ChildScreen("people_few") { navController ->
+ FewPeopleScreen(onResult = { navController.popBackStack() })
+ }
+ val PeopleFull =
+ ChildScreen("people_full") { navController ->
+ FullPeopleScreen(onResult = { navController.popBackStack() })
+ }
+ val People =
+ ParentScreen(
+ "people",
+ mapOf(
+ "Empty" to PeopleEmpty,
+ "Few" to PeopleFew,
+ "Full" to PeopleFull,
+ )
+ )
+
val Home =
ParentScreen(
"home",
@@ -41,20 +63,21 @@
"Material colors" to MaterialColors,
"Android colors" to AndroidColors,
"Example feature" to ExampleFeature,
+ "People" to People,
)
)
}
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
@Composable
-private fun MainContent() {
+private fun MainContent(onControlToggleRequested: () -> Unit) {
Box(Modifier.fillMaxSize()) {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = GalleryAppScreens.Home.identifier,
) {
- screen(GalleryAppScreens.Home, navController)
+ screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
}
}
}
@@ -69,7 +92,7 @@
onChangeTheme: () -> Unit,
) {
val systemFontScale = LocalDensity.current.fontScale
- var fontScale: FontScale by remember {
+ var fontScale: FontScale by rememberSaveable {
mutableStateOf(
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
)
@@ -87,7 +110,7 @@
}
val systemLayoutDirection = LocalLayoutDirection.current
- var layoutDirection by remember { mutableStateOf(systemLayoutDirection) }
+ var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
val onChangeLayoutDirection = {
layoutDirection =
when (layoutDirection) {
@@ -105,19 +128,24 @@
Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
- Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) {
- ConfigurationControls(
- theme,
- fontScale,
- layoutDirection,
- onChangeTheme,
- onChangeLayoutDirection,
- onChangeFontScale,
- )
+ Column(Modifier.fillMaxSize().systemBarsPadding()) {
+ var showControls by rememberSaveable { mutableStateOf(true) }
- Spacer(Modifier.height(4.dp))
+ if (showControls) {
+ ConfigurationControls(
+ theme,
+ fontScale,
+ layoutDirection,
+ onChangeTheme,
+ onChangeLayoutDirection,
+ onChangeFontScale,
+ Modifier.padding(horizontal = 16.dp),
+ )
- MainContent()
+ Spacer(Modifier.height(4.dp))
+ }
+
+ MainContent(onControlToggleRequested = { showControls = !showControls })
}
}
}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
new file mode 100644
index 0000000..2f0df77
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.gallery
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.people.emptyPeopleSpaceViewModel
+import com.android.systemui.people.fewPeopleSpaceViewModel
+import com.android.systemui.people.fullPeopleSpaceViewModel
+import com.android.systemui.people.ui.compose.PeopleScreen
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+
+@Composable
+fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+ val context = LocalContext.current.applicationContext
+ val viewModel = emptyPeopleSpaceViewModel(context)
+ PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+ val context = LocalContext.current.applicationContext
+ val viewModel = fewPeopleSpaceViewModel(context)
+ PeopleScreen(viewModel, onResult)
+}
+
+@Composable
+fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
+ val context = LocalContext.current.applicationContext
+ val viewModel = fullPeopleSpaceViewModel(context)
+ PeopleScreen(viewModel, onResult)
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
index 467dac04..d7d0d72 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
@@ -52,17 +52,29 @@
) : Screen(identifier)
/** Create the navigation graph for [screen]. */
-fun NavGraphBuilder.screen(screen: Screen, navController: NavController) {
+fun NavGraphBuilder.screen(
+ screen: Screen,
+ navController: NavController,
+ onControlToggleRequested: () -> Unit,
+) {
when (screen) {
is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
is ParentScreen -> {
val menuRoute = "${screen.identifier}_menu"
navigation(startDestination = menuRoute, route = screen.identifier) {
// The menu to navigate to one of the children screens.
- composable(menuRoute) { ScreenMenu(screen, navController) }
+ composable(menuRoute) {
+ ScreenMenu(screen, navController, onControlToggleRequested)
+ }
// The content of the child screens.
- screen.children.forEach { (_, child) -> screen(child, navController) }
+ screen.children.forEach { (_, child) ->
+ screen(
+ child,
+ navController,
+ onControlToggleRequested,
+ )
+ }
}
}
}
@@ -72,8 +84,27 @@
private fun ScreenMenu(
screen: ParentScreen,
navController: NavController,
+ onControlToggleRequested: () -> Unit,
) {
- LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ LazyColumn(
+ Modifier.padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ item {
+ Surface(
+ Modifier.fillMaxWidth(),
+ color = MaterialTheme.colorScheme.tertiaryContainer,
+ shape = CircleShape,
+ ) {
+ Column(
+ Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text("Toggle controls")
+ }
+ }
+ }
+
screen.children.forEach { (name, child) ->
item {
Surface(
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
new file mode 100644
index 0000000..0966c32
--- /dev/null
+++ b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
@@ -0,0 +1,156 @@
+/*
+ * 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.people
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.drawable.Icon
+import androidx.core.graphics.drawable.toIcon
+import com.android.systemui.R
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.ui.viewmodel.PeopleViewModel
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** A [PeopleViewModel] that does not have any conversations. */
+fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+ return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
+}
+
+/** A [PeopleViewModel] that has a few conversations. */
+fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+ return fakePeopleSpaceViewModel(
+ context,
+ priorityTiles =
+ listOf(
+ fakeTile(context, id = "0", Color.RED, "Priority"),
+ fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+ ),
+ recentTiles =
+ listOf(
+ fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
+ fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
+ ),
+ )
+}
+
+/** A [PeopleViewModel] that has a lot of conversations. */
+fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
+ return fakePeopleSpaceViewModel(
+ context,
+ priorityTiles =
+ listOf(
+ fakeTile(context, id = "0", Color.RED, "Priority"),
+ fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
+ fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
+ fakeTile(
+ context,
+ id = "3",
+ Color.CYAN,
+ "Priority DndBlocking",
+ isDndBlocking = true,
+ ),
+ fakeTile(
+ context,
+ id = "4",
+ Color.MAGENTA,
+ "Priority NewStory Important",
+ hasNewStory = true,
+ isImportant = true,
+ ),
+ ),
+ recentTiles =
+ listOf(
+ fakeTile(
+ context,
+ id = "5",
+ Color.RED,
+ "Recent NewStory DndBlocking",
+ hasNewStory = true,
+ isDndBlocking = true,
+ ),
+ fakeTile(
+ context,
+ id = "6",
+ Color.BLUE,
+ "Recent Important DndBlocking",
+ isImportant = true,
+ isDndBlocking = true,
+ ),
+ fakeTile(
+ context,
+ id = "7",
+ Color.GREEN,
+ "Recent NewStory Important DndBlocking",
+ hasNewStory = true,
+ isImportant = true,
+ isDndBlocking = true,
+ ),
+ fakeTile(context, id = "8", Color.CYAN, "Recent"),
+ fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
+ ),
+ )
+}
+
+private fun fakePeopleSpaceViewModel(
+ @Application context: Context,
+ priorityTiles: List<PeopleTileModel>,
+ recentTiles: List<PeopleTileModel>,
+): PeopleViewModel {
+ return PeopleViewModel(
+ context,
+ FakePeopleTileRepository(priorityTiles, recentTiles),
+ FakePeopleWidgetRepository(),
+ )
+}
+
+private fun fakeTile(
+ @Application context: Context,
+ id: String,
+ iconColor: Int,
+ username: String,
+ hasNewStory: Boolean = false,
+ isImportant: Boolean = false,
+ isDndBlocking: Boolean = false
+): PeopleTileModel {
+ return PeopleTileModel(
+ PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
+ username,
+ fakeUserIcon(context, iconColor),
+ hasNewStory,
+ isImportant,
+ isDndBlocking,
+ )
+}
+
+private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
+ val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
+ val bitmap =
+ Bitmap.createBitmap(
+ size,
+ size,
+ Bitmap.Config.ARGB_8888,
+ )
+ val canvas = Canvas(bitmap)
+ val paint = Paint().apply { this.color = color }
+ val radius = size / 2f
+ canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
+ return bitmap.toIcon()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
index 0834a5a..e27bfb3 100644
--- a/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/people/ui/viewmodel/PeopleViewModel.kt
@@ -31,7 +31,6 @@
import com.android.systemui.people.data.repository.PeopleTileRepository
import com.android.systemui.people.data.repository.PeopleWidgetRepository
import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -52,7 +51,7 @@
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
*/
private val _priorityTiles = MutableStateFlow(priorityTiles())
- val priorityTiles: Flow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
+ val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow()
/**
* The list of the priority tiles/conversations.
@@ -61,7 +60,7 @@
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
*/
private val _recentTiles = MutableStateFlow(recentTiles())
- val recentTiles: Flow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
+ val recentTiles: StateFlow<List<PeopleTileViewModel>> = _recentTiles.asStateFlow()
/** The ID of the widget currently being edited/added. */
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)