From 66d123f87c9d2b5070e0513b0faa39420dc7db75 Mon Sep 17 00:00:00 2001 From: Jordan Demeulenaere Date: Thu, 21 Jul 2022 15:22:07 +0200 Subject: [PATCH] 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 --- packages/SystemUI/compose/features/Android.bp | 1 + .../compose/features/AndroidManifest.xml | 35 ++- .../people/ui/compose/PeopleScreen.kt | 233 ++++++++++++++++++ .../people/ui/compose/PeopleScreenEmpty.kt | 124 ++++++++++ packages/SystemUI/compose/gallery/Android.bp | 9 + .../systemui/compose/gallery/GalleryApp.kt | 60 +++-- .../systemui/compose/gallery/PeopleScreen.kt | 46 ++++ .../systemui/compose/gallery/Screen.kt | 39 ++- .../src/com/android/systemui/people/Fakes.kt | 156 ++++++++++++ .../people/ui/viewmodel/PeopleViewModel.kt | 5 +- 10 files changed, 683 insertions(+), 25 deletions(-) create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreen.kt create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/people/ui/compose/PeopleScreenEmpty.kt create mode 100644 packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt create mode 100644 packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp index 40218de94258b..325ede613de8e 100644 --- a/packages/SystemUI/compose/features/Android.bp +++ b/packages/SystemUI/compose/features/Android.bp @@ -30,6 +30,7 @@ android_library { ], 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 0aea99d4e9606..eada40e6a40dc 100644 --- a/packages/SystemUI/compose/features/AndroidManifest.xml +++ b/packages/SystemUI/compose/features/AndroidManifest.xml @@ -16,7 +16,38 @@ --> - - + + + + + + + + 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 0000000000000..2bf1937a1c1e8 --- /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, + recentTiles: List, + 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, + 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 0000000000000..5c9358f99858f --- /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 40504dc30c338..b0f5cc1121209 100644 --- a/packages/SystemUI/compose/gallery/Android.bp +++ b/packages/SystemUI/compose/gallery/Android.bp @@ -27,6 +27,7 @@ android_library { srcs: [ "src/**/*.kt", + ":SystemUI-tests-utils", ], resource_dirs: [ @@ -45,6 +46,14 @@ android_library { "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 c341867bfb591..bb98fb350a2e1 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.Composable 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 @@ object GalleryAppScreens { 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 @@ object GalleryAppScreens { "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 @@ fun GalleryApp( 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 @@ fun GalleryApp( } 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 @@ fun GalleryApp( 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 0000000000000..2f0df7790ffd9 --- /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 467dac044b79c..d7d0d721b01c2 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 @@ class ChildScreen( ) : 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 @@ fun NavGraphBuilder.screen(screen: Screen, navController: NavController) { 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 0000000000000..0966c3233ad50 --- /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, + recentTiles: List, +): 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 0834a5ae75f6e..e27bfb34ee3e8 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.model.PeopleTileModel 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 @@ class PeopleViewModel( * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles. */ private val _priorityTiles = MutableStateFlow(priorityTiles()) - val priorityTiles: Flow> = _priorityTiles.asStateFlow() + val priorityTiles: StateFlow> = _priorityTiles.asStateFlow() /** * The list of the priority tiles/conversations. @@ -61,7 +60,7 @@ class PeopleViewModel( * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles. */ private val _recentTiles = MutableStateFlow(recentTiles()) - val recentTiles: Flow> = _recentTiles.asStateFlow() + val recentTiles: StateFlow> = _recentTiles.asStateFlow() /** The ID of the widget currently being edited/added. */ private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)