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

Commit 3934fa76 authored by Wes Okuhara's avatar Wes Okuhara Committed by Android (Google) Code Review
Browse files

Merge "Screen Capture: Setup ViewModel and State" into main

parents ab47c3d2 29556424
Loading
Loading
Loading
Loading
+31 −13
Original line number Diff line number Diff line
@@ -29,11 +29,18 @@ import androidx.compose.ui.unit.dp
import com.android.systemui.common.shared.model.Icon as IconModel
import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.res.R
import com.android.systemui.screencapture.ui.viewmodel.ScreenCaptureRegion
import com.android.systemui.screencapture.ui.viewmodel.ScreenCaptureType
import com.android.systemui.screencapture.ui.viewmodel.ScreenCaptureViewModel

/** TODO(b/422855266): Inject ViewModel */
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun PreCaptureToolbar(expanded: Boolean, onCloseClick: () -> Unit, modifier: Modifier = Modifier) {
@Composable
fun PreCaptureToolbar(
    viewModel: ScreenCaptureViewModel,
    expanded: Boolean,
    onCloseClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    // TODO(b/422855266): Preload icons in the view model to avoid loading icons in UI thread and
    // improve performance
    val screenshotIcon =
@@ -51,18 +58,34 @@ fun PreCaptureToolbar(expanded: Boolean, onCloseClick: () -> Unit, modifier: Mod

    val captureRegionButtonItems =
        listOf(
            RadioButtonGroupItem(icon = screenshotWindowIcon),
            RadioButtonGroupItem(icon = screenshotRegionIcon),
            RadioButtonGroupItem(icon = screenshotFullscreenIcon),
            RadioButtonGroupItem(
                isSelected = (viewModel.captureRegion == ScreenCaptureRegion.APP_WINDOW),
                onClick = { viewModel.updateCaptureRegion(ScreenCaptureRegion.APP_WINDOW) },
                icon = screenshotWindowIcon,
            ),
            RadioButtonGroupItem(
                isSelected = (viewModel.captureRegion == ScreenCaptureRegion.PARTIAL),
                onClick = { viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL) },
                icon = screenshotRegionIcon,
            ),
            RadioButtonGroupItem(
                isSelected = (viewModel.captureRegion == ScreenCaptureRegion.FULLSCREEN),
                onClick = { viewModel.updateCaptureRegion(ScreenCaptureRegion.FULLSCREEN) },
                icon = screenshotFullscreenIcon,
            ),
        )

    val captureTypeButtonItems =
        listOf(
            RadioButtonGroupItem(
                isSelected = (viewModel.captureType == ScreenCaptureType.SCREEN_RECORD),
                onClick = { viewModel.updateCaptureType(ScreenCaptureType.SCREEN_RECORD) },
                icon = screenRecordIcon,
                label = stringResource(id = R.string.screen_capture_toolbar_record_button),
            ),
            RadioButtonGroupItem(
                isSelected = (viewModel.captureType == ScreenCaptureType.SCREENSHOT),
                onClick = { viewModel.updateCaptureType(ScreenCaptureType.SCREENSHOT) },
                icon = screenshotIcon,
                label = stringResource(id = R.string.screen_capture_toolbar_capture_button),
            ),
@@ -80,16 +103,11 @@ fun PreCaptureToolbar(expanded: Boolean, onCloseClick: () -> Unit, modifier: Mod

            Spacer(Modifier.size(8.dp))

            // TODO(b/422855266): Use state from ViewModel for selected index
            RadioButtonGroup(
                items = captureRegionButtonItems,
                selectedIndex = 0,
                onSelect = { _ -> },
            )
            RadioButtonGroup(items = captureRegionButtonItems)

            Spacer(Modifier.size(16.dp))

            RadioButtonGroup(items = captureTypeButtonItems, selectedIndex = 0, onSelect = { _ -> })
            RadioButtonGroup(items = captureTypeButtonItems)
        }
    }
}
+32 −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.ui.compose

import androidx.compose.runtime.Composable
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.screencapture.ui.viewmodel.ScreenCaptureViewModel

/** Main component for the pre-capture UI. */
@Composable
fun PreCaptureUI(viewModelFactory: ScreenCaptureViewModel.Factory) {
    val viewModel: ScreenCaptureViewModel =
        rememberViewModel("ScreenCaptureViewModel") { viewModelFactory.create() }

    PreCaptureToolbar(viewModel = viewModel, expanded = true, onCloseClick = {})

    // TODO: Add region box here
}
+9 −6
Original line number Diff line number Diff line
@@ -42,7 +42,12 @@ private val ICON_SIZE = 20.dp
 * Data class to represent a single radio button item. The item must have an [icon] or a [label] (or
 * both).
 */
data class RadioButtonGroupItem(val icon: IconModel? = null, val label: String? = null) {
data class RadioButtonGroupItem(
    val isSelected: Boolean,
    val onClick: () -> Unit,
    val icon: IconModel? = null,
    val label: String? = null,
) {
    init {
        require(icon != null || label != null) {
            "A ButtonItem must have at least an icon or a label (or both)."
@@ -54,12 +59,10 @@ data class RadioButtonGroupItem(val icon: IconModel? = null, val label: String?
@Composable
fun RadioButtonGroup(
    items: List<RadioButtonGroupItem>,
    selectedIndex: Int,
    onSelect: (index: Int) -> Unit,
    modifier: Modifier = Modifier,
    colors: ToggleButtonColors = defaultColors(),
) {
    require(selectedIndex in 0..items.size) { "selectedIndex is out of range of items." }
    require(items.count { it.isSelected } == 1) { "Only one button item must be selected." }

    Row(
        modifier = modifier,
@@ -74,8 +77,8 @@ fun RadioButtonGroup(
                        items.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
                        else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
                    },
                checked = (index == selectedIndex),
                onCheckedChange = { onSelect(index) },
                checked = item.isSelected,
                onCheckedChange = { item.onClick() },
            ) {
                if (item.icon != null && item.label != null) {
                    Icon(icon = item.icon, modifier = Modifier.size(ICON_SIZE))
+76 −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.ui.viewmodel

import androidx.compose.runtime.getValue
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow

enum class ScreenCaptureType {
    SCREENSHOT,
    SCREEN_RECORD,
}

enum class ScreenCaptureRegion {
    FULLSCREEN,
    PARTIAL,
    APP_WINDOW,
}

/** Models state for the Screen Capture UI */
class ScreenCaptureViewModel @AssistedInject constructor() : ExclusiveActivatable() {
    private val hydrator = Hydrator("ScreenCaptureViewModel.hydrator")

    private val captureTypeSource = MutableStateFlow(ScreenCaptureType.SCREENSHOT)
    private val captureRegionSource = MutableStateFlow(ScreenCaptureRegion.FULLSCREEN)

    // TODO(b/423697394) Init default value to be user's previously selected option
    val captureType by
        hydrator.hydratedStateOf(
            traceName = "captureType",
            initialValue = ScreenCaptureType.SCREENSHOT,
            source = captureTypeSource,
        )

    // TODO(b/423697394) Init default value to be user's previously selected option
    val captureRegion by
        hydrator.hydratedStateOf(
            traceName = "captureRegion",
            initialValue = ScreenCaptureRegion.FULLSCREEN,
            source = captureRegionSource,
        )

    fun updateCaptureType(selectedType: ScreenCaptureType) {
        captureTypeSource.value = selectedType
    }

    fun updateCaptureRegion(selectedRegion: ScreenCaptureRegion) {
        captureRegionSource.value = selectedRegion
    }

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    @AssistedFactory
    interface Factory {
        fun create(): ScreenCaptureViewModel
    }
}
+66 −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.ui.viewmodel

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

@SmallTest
@RunWith(AndroidJUnit4::class)
class ScreenCaptureViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope

    private val viewModel: ScreenCaptureViewModel by lazy { kosmos.screenCaptureViewModel }

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

    @Test
    fun initialState() =
        testScope.runTest {
            // Assert that the initial values are as expected upon creation and activation.
            assertThat(viewModel.captureType).isEqualTo(ScreenCaptureType.SCREENSHOT)
            assertThat(viewModel.captureRegion).isEqualTo(ScreenCaptureRegion.FULLSCREEN)
        }

    @Test
    fun updateCaptureType_updatesState() =
        testScope.runTest {
            viewModel.updateCaptureType(ScreenCaptureType.SCREEN_RECORD)
            assertThat(viewModel.captureType).isEqualTo(ScreenCaptureType.SCREEN_RECORD)
        }

    @Test
    fun updateCaptureRegion_updatesState() =
        testScope.runTest {
            viewModel.updateCaptureRegion(ScreenCaptureRegion.PARTIAL)
            assertThat(viewModel.captureRegion).isEqualTo(ScreenCaptureRegion.PARTIAL)
        }
}
Loading