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

Commit f1b41033 authored by Danny Wang's avatar Danny Wang Committed by Android (Google) Code Review
Browse files

Merge changes Iaf145d94,I721fb476 into main

* changes:
  Screen Sharing: Add ShareContentSelector in the Selector UI
  Screen Sharing: Add ShareContentListViewModel and State
parents 170870c8 25dc994e
Loading
Loading
Loading
Loading
+21 −5
Original line number Diff line number Diff line
@@ -19,18 +19,34 @@ package com.android.systemui.screencapture.sharescreen.largescreen.ui.compose
import androidx.compose.runtime.Composable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.screencapture.common.ui.compose.ScreenCaptureContent
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareViewModel
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareToolbarViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.ShareContentListViewModel
import javax.inject.Inject

class LargeScreenCaptureShareScreenContent
@Inject
constructor(private val viewModelFactory: PreShareViewModel.Factory) : ScreenCaptureContent {
constructor(
    private val preShareToolbarViewModelFactory: PreShareToolbarViewModel.Factory,
    private val shareContentListViewModelFactory: ShareContentListViewModel.Factory,
    private val recentTaskViewModelFactory: RecentTaskViewModel.Factory,
) : ScreenCaptureContent {

    @Composable
    override fun Content() {
        val viewModel: PreShareViewModel =
            rememberViewModel("PreShareViewModel") { viewModelFactory.create() }
        val preShareToolbarViewModel: PreShareToolbarViewModel =
            rememberViewModel("PreShareToolbarViewModel") {
                preShareToolbarViewModelFactory.create()
            }
        val shareContentListViewModel: ShareContentListViewModel =
            rememberViewModel("ShareContentListViewModel") {
                shareContentListViewModelFactory.create()
            }

        PreShareUI(viewModel = viewModel)
        PreShareUI(
            preShareToolbarViewModel = preShareToolbarViewModel,
            shareContentListViewModel = shareContentListViewModel,
            recentTaskViewModelFactory = recentTaskViewModelFactory,
        )
    }
}
+14 −9
Original line number Diff line number Diff line
@@ -32,26 +32,26 @@ import com.android.systemui.screencapture.common.ui.compose.RadioButtonGroup
import com.android.systemui.screencapture.common.ui.compose.RadioButtonGroupItem
import com.android.systemui.screencapture.common.ui.compose.Toolbar
import com.android.systemui.screencapture.common.ui.compose.loadIcon
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareToolbarViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.ScreenShareTarget

@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun PreShareToolbar(
    viewModel: PreShareViewModel,
    preShareToolbarViewModel: PreShareToolbarViewModel,
    expanded: Boolean,
    onCloseClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    val tabIcon by
        loadIcon(
            viewModel = viewModel,
            viewModel = preShareToolbarViewModel,
            resId = R.drawable.ic_screen_capture_tab,
            contentDescription = null,
        )
    val windowIcon by
        loadIcon(
            viewModel = viewModel,
            viewModel = preShareToolbarViewModel,
            resId = R.drawable.ic_screen_capture_window,
            contentDescription = null,
        )
@@ -59,13 +59,18 @@ fun PreShareToolbar(
        listOf(
            RadioButtonGroupItem(
                icon = tabIcon,
                isSelected = viewModel.selectedScreenShareTarget == ScreenShareTarget.TAB,
                onClick = { viewModel.onTargetSelected(ScreenShareTarget.TAB) },
                isSelected =
                    preShareToolbarViewModel.selectedScreenShareTarget == ScreenShareTarget.TAB,
                onClick = { preShareToolbarViewModel.onTargetSelected(ScreenShareTarget.TAB) },
            ),
            RadioButtonGroupItem(
                icon = windowIcon,
                isSelected = viewModel.selectedScreenShareTarget == ScreenShareTarget.APP_WINDOW,
                onClick = { viewModel.onTargetSelected(ScreenShareTarget.APP_WINDOW) },
                isSelected =
                    preShareToolbarViewModel.selectedScreenShareTarget ==
                        ScreenShareTarget.APP_WINDOW,
                onClick = {
                    preShareToolbarViewModel.onTargetSelected(ScreenShareTarget.APP_WINDOW)
                },
            ),
        )

@@ -77,7 +82,7 @@ fun PreShareToolbar(

            val shareIcon by
                loadIcon(
                    viewModel = viewModel,
                    viewModel = preShareToolbarViewModel,
                    resId = R.drawable.ic_present_to_all,
                    ContentDescription.Resource(R.string.screen_share_toolbar_share_button),
                )
+29 −5
Original line number Diff line number Diff line
@@ -16,13 +16,37 @@

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

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareViewModel
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.PreShareToolbarViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.ShareContentListViewModel

/** Main component for the screen share UI. */
@Composable
fun PreShareUI(viewModel: PreShareViewModel) {
    PreShareToolbar(viewModel = viewModel, expanded = true, onCloseClick = {})

    // TODO: Add PreShareSelector here.
fun PreShareUI(
    preShareToolbarViewModel: PreShareToolbarViewModel,
    shareContentListViewModel: ShareContentListViewModel,
    recentTaskViewModelFactory: RecentTaskViewModel.Factory,
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Top),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier.size(800.dp, 600.dp),
    ) {
        PreShareToolbar(
            preShareToolbarViewModel = preShareToolbarViewModel,
            expanded = true,
            onCloseClick = {},
        )
        ShareContentSelector(
            shareContentListViewModel = shareContentListViewModel,
            recentTaskViewModelFactory = recentTaskViewModelFactory,
        )
    }
}
+56 −54
Original line number Diff line number Diff line
@@ -16,10 +16,10 @@

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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
@@ -27,71 +27,83 @@ 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
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 =
@@ -103,12 +115,16 @@ private fun SelectorItem(
            modifier = Modifier.padding(12.dp).clickable(onClick = onItemSelected),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Image(
                // TODO: Address the hardcoded placeholder color.
                bitmap = icon?.asImageBitmap() ?: createDefaultColorImageBitmap(20, 20, Color.BLUE),
                contentDescription = null,
                modifier = Modifier.size(16.dp).clip(CircleShape),
            )
            Box(
                modifier =
                    Modifier.size(16.dp)
                        .clip(CircleShape)
                        .background(MaterialTheme.colorScheme.surfaceVariant)
            ) {
                if (icon != null) {
                    Image(bitmap = icon.asImageBitmap(), contentDescription = label?.toString())
                }
            }
            Spacer(modifier = Modifier.width(8.dp))
            Text(
                text = label?.toString() ?: "Title",
@@ -118,17 +134,3 @@ private fun SelectorItem(
        }
    }
}

/**
 * Creates an [ImageBitmap] of a given size, filled with a solid color. Used as a placeholder for
 * when an app icon is not available.
 *
 * @param width The width of the bitmap.
 * @param height The height of the bitmap.
 * @param color The color to fill the bitmap with.
 */
private fun createDefaultColorImageBitmap(width: Int, height: Int, color: Int): ImageBitmap {
    val bitmap = createBitmap(width, height)
    bitmap.eraseColor(color)
    return bitmap.asImageBitmap()
}
+118 −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.compose

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.sharescreen.largescreen.ui.viewmodel.ShareContentListViewModel

@Composable
fun ShareContentSelector(
    shareContentListViewModel: ShareContentListViewModel,
    recentTaskViewModelFactory: RecentTaskViewModel.Factory,
) {
    val selectedRecentTaskViewModel = shareContentListViewModel.selectedRecentTaskViewModel

    Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = RoundedCornerShape(28.dp)) {
        Column(modifier = Modifier.padding(8.dp)) {
            Text(text = "Share an app", modifier = Modifier.padding(12.dp, 8.dp).fillMaxWidth())
            Spacer(modifier = Modifier.height(16.dp))
            Row(modifier = Modifier.padding(8.dp)) {
                // The sharing content item list.
                ShareContentList(
                    viewModel = shareContentListViewModel,
                    recentTaskViewModelFactory = recentTaskViewModelFactory,
                    selectedRecentTaskViewModel = selectedRecentTaskViewModel,
                )
                Spacer(modifier = Modifier.width(10.dp))
                ItemPreview(
                    preview = selectedRecentTaskViewModel?.thumbnail?.getOrNull()?.asImageBitmap(),
                    modifier = Modifier.weight(1f).height(120.dp),
                )
            }
            DisclaimerText()
            AudioSwitch()
        }
    }
}

@Composable
private fun ItemPreview(preview: ImageBitmap?, modifier: Modifier = Modifier) {
    Box(
        modifier =
            modifier
                .clip(RoundedCornerShape(16.dp))
                .background(MaterialTheme.colorScheme.surfaceVariant),
        contentAlignment = Alignment.Center,
    ) {
        if (preview != null) {
            Image(
                bitmap = preview,
                contentDescription = "preview",
                modifier = Modifier.matchParentSize(),
                contentScale = ContentScale.Fit,
            )
        }
    }
}

@Composable
private fun DisclaimerText() {
    Text(
        text =
            "Disclaimer When you’re sharing your entire screen, anything shown on your screen" +
                " is recorded. So be careful with things like passwords, payment details," +
                " messages, photos, and audio & video.",
        style = MaterialTheme.typography.bodySmall,
        modifier = Modifier.padding(12.dp, 8.dp).fillMaxWidth(),
    )
}

@Composable
private fun AudioSwitch() {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.SpaceBetween,
        modifier = Modifier.padding(12.dp, 0.dp).fillMaxWidth(),
    ) {
        Text(text = "Share audio", style = MaterialTheme.typography.labelMedium)
        Switch(checked = true, onCheckedChange = {}, modifier = Modifier.height(24.dp))
    }
}
Loading