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

Commit 6024882b authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Screen Capture: Load icons in background and create button view model" into main

parents 7c1cb058 bfd4d32b
Loading
Loading
Loading
Loading
+13 −53
Original line number Diff line number Diff line
@@ -24,13 +24,8 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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

@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -41,55 +36,20 @@ fun PreCaptureToolbar(
    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 =
        IconModel.Resource(res = R.drawable.ic_screen_capture_camera, contentDescription = null)
    val screenRecordIcon =
        IconModel.Resource(res = R.drawable.ic_screenrecord, contentDescription = null)
    val screenshotFullscreenIcon =
        IconModel.Resource(res = R.drawable.ic_screen_capture_fullscreen, contentDescription = null)
    val screenshotRegionIcon =
        IconModel.Resource(res = R.drawable.ic_screen_capture_region, contentDescription = null)
    val screenshotWindowIcon =
        IconModel.Resource(res = R.drawable.ic_screen_capture_window, contentDescription = null)
    val moreOptionsIcon =
        IconModel.Resource(res = R.drawable.ic_more_vert, contentDescription = null)

    val captureRegionButtonItems =
        listOf(
            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),
            ),
        viewModel.captureTypeButtonViewModels.map {
            RadioButtonGroupItem(
                isSelected = (viewModel.captureType == ScreenCaptureType.SCREENSHOT),
                onClick = { viewModel.updateCaptureType(ScreenCaptureType.SCREENSHOT) },
                icon = screenshotIcon,
                label = stringResource(id = R.string.screen_capture_toolbar_capture_button),
            ),
                label = it.label,
                icon = it.icon,
                isSelected = it.isSelected,
                onClick = it.onClick,
            )
        }

    val captureRegionButtonItems =
        viewModel.captureRegionButtonViewModels.map {
            RadioButtonGroupItem(icon = it.icon, isSelected = it.isSelected, onClick = it.onClick)
        }

    Toolbar(expanded = expanded, onCloseClick = onCloseClick, modifier = modifier) {
        Row {
@@ -98,7 +58,7 @@ fun PreCaptureToolbar(
                onCheckedChange = {},
                shape = IconButtonDefaults.smallSquareShape,
            ) {
                Icon(icon = moreOptionsIcon)
                viewModel.icons?.let { Icon(icon = it.moreOptions) }
            }

            Spacer(Modifier.size(8.dp))
+1 −7
Original line number Diff line number Diff line
@@ -47,13 +47,7 @@ data class RadioButtonGroupItem(
    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)."
        }
    }
}
)

/** A group of N icon buttons where any single icon button is selected at a time. */
@Composable
+27 −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 com.android.systemui.common.shared.model.Icon

/** Models a single button within the RadioButtonGroup. */
data class RadioButtonGroupItemViewModel(
    val label: String? = null,
    val icon: Icon? = null,
    val isSelected: Boolean,
    val onClick: () -> Unit,
)
+79 −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 android.annotation.DrawableRes
import android.annotation.SuppressLint
import android.content.Context
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.UiBackground
import com.android.systemui.res.R
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext

data class ScreenCaptureIcons(
    val screenshot: Icon,
    val screenRecord: Icon,
    val fullscreen: Icon,
    val region: Icon,
    val appWindow: Icon,
    val moreOptions: Icon,
)

class ScreenCaptureIconProvider
@Inject
constructor(
    @Application private val context: Context,
    @UiBackground private val uiBackgroundContext: CoroutineContext,
) {
    private val _icons = MutableStateFlow<ScreenCaptureIcons?>(null)

    /** Static set of icons used in the UI. */
    val icons = _icons.asStateFlow()

    /** Loads all icon drawables in the UI background thread and emits them to [icons]. */
    suspend fun collectIcons() {
        flow {
                emit(
                    ScreenCaptureIcons(
                        screenshot = loadIcon(R.drawable.ic_screen_capture_camera),
                        screenRecord = loadIcon(R.drawable.ic_screenrecord),
                        fullscreen = loadIcon(R.drawable.ic_screen_capture_fullscreen),
                        region = loadIcon(R.drawable.ic_screen_capture_region),
                        appWindow = loadIcon(R.drawable.ic_screen_capture_window),
                        moreOptions = loadIcon(R.drawable.ic_more_vert),
                    )
                )
            }
            .collect(_icons)
    }

    /**
     * Load the icon drawables in the UI background thread to avoid loading them on the main UI
     * thread which can cause UI jank or dropped frames.
     */
    @SuppressLint("UseCompatLoadingForDrawables")
    private suspend fun loadIcon(@DrawableRes resourceId: Int): Icon.Loaded {
        val drawable = withContext(uiBackgroundContext) { context.getDrawable(resourceId)!! }
        return Icon.Loaded(drawable = drawable, res = resourceId, contentDescription = null)
    }
}
+79 −20
Original line number Diff line number Diff line
@@ -16,12 +16,16 @@

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 android.content.Context
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.lifecycle.HydratedActivatable
import com.android.systemui.res.R
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch

enum class ScreenCaptureType {
    SCREENSHOT,
@@ -35,26 +39,38 @@ enum class ScreenCaptureRegion {
}

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

class ScreenCaptureViewModel
@AssistedInject
constructor(
    @Application private val context: Context,
    private val iconProvider: ScreenCaptureIconProvider,
) : HydratedActivatable() {
    private val captureTypeSource = MutableStateFlow(ScreenCaptureType.SCREENSHOT)
    private val captureRegionSource = MutableStateFlow(ScreenCaptureRegion.FULLSCREEN)

    val icons: ScreenCaptureIcons? by iconProvider.icons.hydratedStateOf()

    // 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,
        )
    val captureType: ScreenCaptureType by captureTypeSource.hydratedStateOf()

    // 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,
    val captureRegion: ScreenCaptureRegion by captureRegionSource.hydratedStateOf()

    val captureTypeButtonViewModels: List<RadioButtonGroupItemViewModel> by
        combine(captureTypeSource, iconProvider.icons) { selectedType, icons ->
                generateCaptureTypeButtonViewModels(selectedType, icons)
            }
            .hydratedStateOf(
                initialValue = generateCaptureTypeButtonViewModels(captureTypeSource.value, null)
            )

    val captureRegionButtonViewModels: List<RadioButtonGroupItemViewModel> by
        combine(captureRegionSource, iconProvider.icons) { selectedRegion, icons ->
                generateCaptureRegionButtonViewModels(selectedRegion, icons)
            }
            .hydratedStateOf(
                initialValue =
                    generateCaptureRegionButtonViewModels(captureRegionSource.value, null)
            )

    fun updateCaptureType(selectedType: ScreenCaptureType) {
@@ -65,8 +81,51 @@ class ScreenCaptureViewModel @AssistedInject constructor() : ExclusiveActivatabl
        captureRegionSource.value = selectedRegion
    }

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    override suspend fun onActivated() {
        coroutineScope { launch { iconProvider.collectIcons() } }
    }

    private fun generateCaptureTypeButtonViewModels(
        selectedType: ScreenCaptureType,
        icons: ScreenCaptureIcons?,
    ): List<RadioButtonGroupItemViewModel> {
        return listOf(
            RadioButtonGroupItemViewModel(
                icon = icons?.screenRecord,
                label = context.getString(R.string.screen_capture_toolbar_record_button),
                isSelected = selectedType == ScreenCaptureType.SCREEN_RECORD,
                onClick = { updateCaptureType(ScreenCaptureType.SCREEN_RECORD) },
            ),
            RadioButtonGroupItemViewModel(
                icon = icons?.screenshot,
                label = context.getString(R.string.screen_capture_toolbar_capture_button),
                isSelected = selectedType == ScreenCaptureType.SCREENSHOT,
                onClick = { updateCaptureType(ScreenCaptureType.SCREENSHOT) },
            ),
        )
    }

    private fun generateCaptureRegionButtonViewModels(
        selectedRegion: ScreenCaptureRegion,
        icons: ScreenCaptureIcons?,
    ): List<RadioButtonGroupItemViewModel> {
        return listOf(
            RadioButtonGroupItemViewModel(
                icon = icons?.appWindow,
                isSelected = (selectedRegion == ScreenCaptureRegion.APP_WINDOW),
                onClick = { updateCaptureRegion(ScreenCaptureRegion.APP_WINDOW) },
            ),
            RadioButtonGroupItemViewModel(
                icon = icons?.region,
                isSelected = (selectedRegion == ScreenCaptureRegion.PARTIAL),
                onClick = { updateCaptureRegion(ScreenCaptureRegion.PARTIAL) },
            ),
            RadioButtonGroupItemViewModel(
                icon = icons?.fullscreen,
                isSelected = (selectedRegion == ScreenCaptureRegion.FULLSCREEN),
                onClick = { updateCaptureRegion(ScreenCaptureRegion.FULLSCREEN) },
            ),
        )
    }

    @AssistedFactory
Loading