Merge "Implement the People UI in Compose (1/2)" into tm-qpr-dev

This commit is contained in:
Jordan Demeulenaere
2022-08-21 15:42:48 +00:00
committed by Android (Google) Code Review
10 changed files with 683 additions and 25 deletions

View File

@@ -30,6 +30,7 @@ android_library {
], ],
static_libs: [ static_libs: [
"SystemUI-core",
"SystemUIComposeCore", "SystemUIComposeCore",
"androidx.compose.runtime_runtime", "androidx.compose.runtime_runtime",

View File

@@ -16,7 +16,38 @@
--> -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.android.systemui.compose.features"> 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> </manifest>

View File

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

View File

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

View File

@@ -27,6 +27,7 @@ android_library {
srcs: [ srcs: [
"src/**/*.kt", "src/**/*.kt",
":SystemUI-tests-utils",
], ],
resource_dirs: [ resource_dirs: [
@@ -45,6 +46,14 @@ android_library {
"androidx.navigation_navigation-compose", "androidx.navigation_navigation-compose",
"androidx.appcompat_appcompat", "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"], kotlincflags: ["-Xjvm-default=all"],

View File

@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -33,6 +33,28 @@ object GalleryAppScreens {
val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() } val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() } 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 = val Home =
ParentScreen( ParentScreen(
"home", "home",
@@ -41,20 +63,21 @@ object GalleryAppScreens {
"Material colors" to MaterialColors, "Material colors" to MaterialColors,
"Android colors" to AndroidColors, "Android colors" to AndroidColors,
"Example feature" to ExampleFeature, "Example feature" to ExampleFeature,
"People" to People,
) )
) )
} }
/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */ /** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
@Composable @Composable
private fun MainContent() { private fun MainContent(onControlToggleRequested: () -> Unit) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
val navController = rememberNavController() val navController = rememberNavController()
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = GalleryAppScreens.Home.identifier, startDestination = GalleryAppScreens.Home.identifier,
) { ) {
screen(GalleryAppScreens.Home, navController) screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
} }
} }
} }
@@ -69,7 +92,7 @@ fun GalleryApp(
onChangeTheme: () -> Unit, onChangeTheme: () -> Unit,
) { ) {
val systemFontScale = LocalDensity.current.fontScale val systemFontScale = LocalDensity.current.fontScale
var fontScale: FontScale by remember { var fontScale: FontScale by rememberSaveable {
mutableStateOf( mutableStateOf(
FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
) )
@@ -87,7 +110,7 @@ fun GalleryApp(
} }
val systemLayoutDirection = LocalLayoutDirection.current val systemLayoutDirection = LocalLayoutDirection.current
var layoutDirection by remember { mutableStateOf(systemLayoutDirection) } var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
val onChangeLayoutDirection = { val onChangeLayoutDirection = {
layoutDirection = layoutDirection =
when (layoutDirection) { when (layoutDirection) {
@@ -105,19 +128,24 @@ fun GalleryApp(
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background, color = MaterialTheme.colorScheme.background,
) { ) {
Column(Modifier.fillMaxSize().systemBarsPadding().padding(16.dp)) { Column(Modifier.fillMaxSize().systemBarsPadding()) {
ConfigurationControls( var showControls by rememberSaveable { mutableStateOf(true) }
theme,
fontScale,
layoutDirection,
onChangeTheme,
onChangeLayoutDirection,
onChangeFontScale,
)
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 })
} }
} }
} }

View File

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

View File

@@ -52,17 +52,29 @@ class ChildScreen(
) : Screen(identifier) ) : Screen(identifier)
/** Create the navigation graph for [screen]. */ /** Create the navigation graph for [screen]. */
fun NavGraphBuilder.screen(screen: Screen, navController: NavController) { fun NavGraphBuilder.screen(
screen: Screen,
navController: NavController,
onControlToggleRequested: () -> Unit,
) {
when (screen) { when (screen) {
is ChildScreen -> composable(screen.identifier) { screen.content(navController) } is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
is ParentScreen -> { is ParentScreen -> {
val menuRoute = "${screen.identifier}_menu" val menuRoute = "${screen.identifier}_menu"
navigation(startDestination = menuRoute, route = screen.identifier) { navigation(startDestination = menuRoute, route = screen.identifier) {
// The menu to navigate to one of the children screens. // 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. // 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( private fun ScreenMenu(
screen: ParentScreen, screen: ParentScreen,
navController: NavController, 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) -> screen.children.forEach { (name, child) ->
item { item {
Surface( Surface(

View File

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

View File

@@ -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.PeopleTileRepository
import com.android.systemui.people.data.repository.PeopleWidgetRepository import com.android.systemui.people.data.repository.PeopleWidgetRepository
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -52,7 +51,7 @@ class PeopleViewModel(
* reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles. * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
*/ */
private val _priorityTiles = MutableStateFlow(priorityTiles()) 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. * 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. * reactive and you have to manually call [onTileRefreshRequested] to refresh the tiles.
*/ */
private val _recentTiles = MutableStateFlow(recentTiles()) 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. */ /** The ID of the widget currently being edited/added. */
private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID) private val _appWidgetId = MutableStateFlow(INVALID_APPWIDGET_ID)