Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e5c5de9a authored by Danny Wang's avatar Danny Wang
Browse files

Screen Sharing: Add ShareContentListViewModel and State

Basic setup for the ShareContentListViewModel which holds the state for
the content list UI. The state for the content items are propagated
down to the composables.

BUG: 436886242
Test: atest ShareContentListViewModelTest
Flag: com.android.systemui.large_screen_sharing
Change-Id: I721fb476f51785a715530772ef848eed3ff34bea
parent b65abf84
Loading
Loading
Loading
Loading
+45 −32
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package com.android.systemui.screencapture.sharescreen.largescreen.ui.compose

import android.graphics.Bitmap
import android.graphics.Color
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
@@ -27,13 +26,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -41,57 +41,70 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap

/**
 * A temporary data class representing a single item in the list. This will be replaced by a
 * ViewModel in the next step.
 */
private data class ContentItem(val icon: Bitmap?, val label: CharSequence?)
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.ShareContentListViewModel

/**
 * A composable that displays a scrollable list of shareable content (e.g., recent apps).
 *
 * @param modifier The modifier to be applied to the composable.
 * @param viewModel The ViewModel that provides the list of tasks and manages selection state.
 * @param recentTaskViewModelFactory A factory to create a [RecentTaskViewModel] for each item.
 * @param selectedRecentTaskViewModel The selected RecentTaskViewModel.
 */
@Composable
fun ShareContentList(modifier: Modifier = Modifier) {
    // TODO(b/436886242): Remove dummy data and inject view model.
    val contentList =
        listOf(
            ContentItem(icon = null, label = "App 1"),
            ContentItem(icon = null, label = "App 2"),
            ContentItem(icon = null, label = "App 3"),
            ContentItem(icon = null, label = "App 4"),
        )
fun ShareContentList(
    modifier: Modifier = Modifier,
    viewModel: ShareContentListViewModel,
    recentTaskViewModelFactory: RecentTaskViewModel.Factory,
    selectedRecentTaskViewModel: RecentTaskViewModel?,
) {
    val recentTasks by viewModel.recentTasks.collectAsStateWithLifecycle(initialValue = null)

    LazyColumn(modifier = modifier.height(120.dp).width(148.dp)) {
        itemsIndexed(contentList) { index, contentItem ->
        // Use the real list of recent tasks, handling the nullable case.
        recentTasks?.let { tasks ->
            items(items = tasks) { task ->
                val currentRecentTaskViewModel: RecentTaskViewModel =
                    rememberViewModel(
                        traceName = "ShareContentListItemViewModel#${task.taskId}",
                        key = task,
                    ) {
                        recentTaskViewModelFactory.create(task)
                    }
                SelectorItem(
                icon = contentItem.icon,
                label = contentItem.label,
                isSelected = index == 0,
                onItemSelected = {},
                    currentRecentTaskViewModel = currentRecentTaskViewModel,
                    isSelected =
                        currentRecentTaskViewModel.task == selectedRecentTaskViewModel?.task,
                    onItemSelected = {
                        viewModel.selectedRecentTaskViewModel = currentRecentTaskViewModel
                    },
                )
            }
        }
    }
}

/**
 * A composable that displays a single item in the share content list. It shows an icon and a label,
 * and its appearance changes based on whether it is selected.
 * A composable that displays a single item in the share content list.
 *
 * @param icon The icon to display for the item. A placeholder is used if null.
 * @param label The text label for the item. A placeholder is used if null.
 * @param isSelected Whether this item is currently selected.
 * @param currentRecentTaskViewModel The [RecentTaskViewModel] that holds the state for this
 *   specific item.
 * @param isSelected The boolean if the currentRecentTaskViewModel is selected.
 * @param onItemSelected The callback to be invoked when this item is clicked.
 */
@Composable
private fun SelectorItem(
    icon: Bitmap?,
    label: CharSequence?,
    currentRecentTaskViewModel: RecentTaskViewModel,
    isSelected: Boolean,
    onItemSelected: () -> Unit,
) {
    // Get the icon and label from the item's ViewModel.
    val icon = currentRecentTaskViewModel.icon?.getOrNull()
    val label = currentRecentTaskViewModel.label?.getOrNull()

    Surface(
        shape = RoundedCornerShape(20.dp),
        color =
@@ -106,7 +119,7 @@ private fun SelectorItem(
            Image(
                // TODO: Address the hardcoded placeholder color.
                bitmap = icon?.asImageBitmap() ?: createDefaultColorImageBitmap(20, 20, Color.BLUE),
                contentDescription = null,
                contentDescription = label?.toString(),
                modifier = Modifier.size(16.dp).clip(CircleShape),
            )
            Spacer(modifier = Modifier.width(8.dp))
+45 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.screencapture.sharescreen.largescreen.ui.viewmodel

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.android.systemui.lifecycle.HydratedActivatable
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTasksViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/**
 * ViewModel for the Share Content list UI.
 *
 * This class follows a composition pattern. It delegates the responsibility of providing the recent
 * tasks list via the [RecentTasksViewModel] interface, and adds its own UI-specific state
 * management, such as tracking the selected item.
 */
class ShareContentListViewModel
@AssistedInject
constructor(private val recentTasksViewModel: RecentTasksViewModel) :
    HydratedActivatable(), RecentTasksViewModel by recentTasksViewModel {
    var selectedRecentTaskViewModel by mutableStateOf<RecentTaskViewModel?>(null)

    @AssistedFactory
    interface Factory {
        fun create(): ShareContentListViewModel
    }
}
+50 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.screencapture.sharescreen.largescreen.ui.viewmodel

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.testKosmosNew
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class ShareContentListViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmosNew()
    private val testScope = kosmos.testScope

    private val viewModel: ShareContentListViewModel = kosmos.fakeShareContentListViewModel

    @Before
    fun setUp() {
        viewModel.activateIn(testScope)
    }

    @Test
    fun initialState() =
        kosmos.runTest {
            // Assert that the initial values are as expected upon creation and activation.
            assertThat(viewModel.selectedRecentTaskViewModel).isEqualTo(null)
        }
}
+26 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.screencapture.sharescreen.largescreen.ui.viewmodel

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.screencapture.common.ui.viewmodel.fakeRecentTasksViewModel

/** A Kosmos fixture for a fake [ShareContentListViewModel] that can be used in tests. */
val Kosmos.fakeShareContentListViewModel by Fixture {
    ShareContentListViewModel(recentTasksViewModel = fakeRecentTasksViewModel)
}