diff options
10 files changed, 683 insertions, 25 deletions
diff --git a/packages/SystemUI/compose/features/Android.bp b/packages/SystemUI/compose/features/Android.bp index 40218de94258..325ede613de8 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 0aea99d4e960..eada40e6a40d 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" - package="com.android.systemui.compose.features"> - + 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 000000000000..2bf1937a1c1e --- /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 000000000000..5c9358f99858 --- /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 40504dc30c33..b0f5cc112120 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 c341867bfb59..bb98fb350a2e 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) } + + if (showControls) { + ConfigurationControls( + theme, + fontScale, + layoutDirection, + onChangeTheme, + onChangeLayoutDirection, + onChangeFontScale, + Modifier.padding(horizontal = 16.dp), + ) - Spacer(Modifier.height(4.dp)) + Spacer(Modifier.height(4.dp)) + } - MainContent() + 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 000000000000..2f0df7790ffd --- /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 467dac044b79..d7d0d721b01c 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 000000000000..0966c3233ad5 --- /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 0834a5ae75f6..e27bfb34ee3e 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<List<PeopleTileViewModel>> = _priorityTiles.asStateFlow() + val priorityTiles: StateFlow<List<PeopleTileViewModel>> = _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<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) |