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

Commit a634cb2c authored by Anton Potapov's avatar Anton Potapov
Browse files

Add app selector menu

This change add app selection carousel to choose an app to be recorded.

Flag: com.android.systemui.new_screen_record_toolbar
Bug: 428171948
Test: manual on foldable: check that the app can be selected.
Change-Id: I1a1936ab2dffecfbdb5ae1d6f17f9c49b22115b7
parent 48a40537
Loading
Loading
Loading
Loading
+25 −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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="16dp"
    android:height="16dp"
    android:viewportWidth="16"
    android:viewportHeight="16">
  <path
      android:pathData="M2,16C1.45,16 0.975,15.808 0.575,15.425C0.192,15.025 0,14.55 0,14C0,13.45 0.192,12.983 0.575,12.6C0.975,12.2 1.45,12 2,12C2.55,12 3.017,12.2 3.4,12.6C3.8,12.983 4,13.45 4,14C4,14.55 3.8,15.025 3.4,15.425C3.017,15.808 2.55,16 2,16ZM8,16C7.45,16 6.975,15.808 6.575,15.425C6.192,15.025 6,14.55 6,14C6,13.45 6.192,12.983 6.575,12.6C6.975,12.2 7.45,12 8,12C8.55,12 9.017,12.2 9.4,12.6C9.8,12.983 10,13.45 10,14C10,14.55 9.8,15.025 9.4,15.425C9.017,15.808 8.55,16 8,16ZM14,16C13.45,16 12.975,15.808 12.575,15.425C12.192,15.025 12,14.55 12,14C12,13.45 12.192,12.983 12.575,12.6C12.975,12.2 13.45,12 14,12C14.55,12 15.017,12.2 15.4,12.6C15.8,12.983 16,13.45 16,14C16,14.55 15.8,15.025 15.4,15.425C15.017,15.808 14.55,16 14,16ZM2,10C1.45,10 0.975,9.808 0.575,9.425C0.192,9.025 0,8.55 0,8C0,7.45 0.192,6.983 0.575,6.6C0.975,6.2 1.45,6 2,6C2.55,6 3.017,6.2 3.4,6.6C3.8,6.983 4,7.45 4,8C4,8.55 3.8,9.025 3.4,9.425C3.017,9.808 2.55,10 2,10ZM8,10C7.45,10 6.975,9.808 6.575,9.425C6.192,9.025 6,8.55 6,8C6,7.45 6.192,6.983 6.575,6.6C6.975,6.2 7.45,6 8,6C8.55,6 9.017,6.2 9.4,6.6C9.8,6.983 10,7.45 10,8C10,8.55 9.8,9.025 9.4,9.425C9.017,9.808 8.55,10 8,10ZM14,10C13.45,10 12.975,9.808 12.575,9.425C12.192,9.025 12,8.55 12,8C12,7.45 12.192,6.983 12.575,6.6C12.975,6.2 13.45,6 14,6C14.55,6 15.017,6.2 15.4,6.6C15.8,6.983 16,7.45 16,8C16,8.55 15.8,9.025 15.4,9.425C15.017,9.808 14.55,10 14,10ZM2,4C1.45,4 0.975,3.808 0.575,3.425C0.192,3.025 0,2.55 0,2C0,1.45 0.192,0.983 0.575,0.6C0.975,0.2 1.45,-0 2,-0C2.55,-0 3.017,0.2 3.4,0.6C3.8,0.983 4,1.45 4,2C4,2.55 3.8,3.025 3.4,3.425C3.017,3.808 2.55,4 2,4ZM8,4C7.45,4 6.975,3.808 6.575,3.425C6.192,3.025 6,2.55 6,2C6,1.45 6.192,0.983 6.575,0.6C6.975,0.2 7.45,-0 8,-0C8.55,-0 9.017,0.2 9.4,0.6C9.8,0.983 10,1.45 10,2C10,2.55 9.8,3.025 9.4,3.425C9.017,3.808 8.55,4 8,4ZM14,4C13.45,4 12.975,3.808 12.575,3.425C12.192,3.025 12,2.55 12,2C12,1.45 12.192,0.983 12.575,0.6C12.975,0.2 13.45,-0 14,-0C14.55,-0 15.017,0.2 15.4,0.6C15.8,0.983 16,1.45 16,2C16,2.55 15.8,3.025 15.4,3.425C15.017,3.808 14.55,4 14,4Z"
      android:fillColor="#ffffff"/>
</vector>
+25 −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.
  -->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="6dp"
    android:height="8dp"
    android:viewportHeight="8"
    android:viewportWidth="6">
    <path
        android:fillColor="#ffffff"
        android:pathData="M3.7,4L0.9667,1.2667C0.8444,1.1444 0.7833,1.0056 0.7833,0.85C0.7833,0.6833 0.8444,0.5389 0.9667,0.4167C1.0889,0.2944 1.2278,0.2333 1.3833,0.2333C1.55,0.2333 1.6944,0.2944 1.8167,0.4167L4.9833,3.5833C5.0389,3.6389 5.0778,3.7055 5.1,3.7833C5.1333,3.85 5.15,3.9222 5.15,4C5.15,4.0778 5.1333,4.1556 5.1,4.2333C5.0778,4.3 5.0389,4.3611 4.9833,4.4167L1.8167,7.5833C1.6944,7.7056 1.5556,7.7667 1.4,7.7667C1.2444,7.7556 1.1056,7.6889 0.9833,7.5667C0.8611,7.4444 0.8,7.3056 0.8,7.15C0.8,6.9833 0.8611,6.8389 0.9833,6.7167L3.7,4Z" />
</vector>
+2 −0
Original line number Diff line number Diff line
@@ -352,6 +352,8 @@
    <string name="screen_record_entire_screen">Entire screen</string>
    <!-- Screen recording should record a single app [CHAR LIMIT=35] -->
    <string name="screen_record_single_app">Single app</string>
    <!-- Screen recording hint shown on a button leading to choosing an app to record [CHAR LIMIT=35] -->
    <string name="screen_record_single_app_hint">App to record</string>
    <!-- Screen recording should record a single app but there are no apps open [CHAR LIMIT=35] -->
    <string name="screen_record_single_app_no_recents">Single app (Open an app to choose)</string>
    <!-- Screen recording should record device audio setting [CHAR LIMIT=35] -->
+4 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.systemui.screencapture.common

import android.view.Display
import com.android.systemui.screencapture.common.shared.model.ScreenCaptureUiParameters
import com.android.systemui.screencapture.common.ui.compose.ScreenCaptureContent
import dagger.BindsInstance
@@ -50,6 +51,9 @@ interface ScreenCaptureUiComponent {
        @BindsInstance
        fun setParameters(@ScreenCaptureUi parameters: ScreenCaptureUiParameters): Builder

        /** [Display] that hosts the Screen Capture UI. */
        @BindsInstance fun setDisplay(@ScreenCaptureUi display: Display): Builder

        /**
         * Builds this [ScreenCaptureUiComponent]. Actual Subcomponent Builders should override this
         * method with their own version that returns the actual subcomponent type.
+76 −48
Original line number Diff line number Diff line
@@ -14,17 +14,15 @@
 * limitations under the License.
 */

@file:OptIn(ExperimentalMaterial3Api::class)

package com.android.systemui.screencapture.record.smallscreen.ui.compose

import android.graphics.Bitmap
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -36,33 +34,33 @@ 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.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.carousel.CarouselDefaults
import androidx.compose.material3.carousel.CarouselItemScope
import androidx.compose.material3.carousel.HorizontalUncontainedCarousel
import androidx.compose.material3.carousel.rememberCarouselState
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.asImageBitmap
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.android.compose.PlatformIconButton
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.screencapture.common.domain.model.ScreenCaptureRecentTask
import com.android.systemui.screencapture.common.ui.viewmodel.RecentTaskViewModel
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.RecordDetailsAppSelectorViewModel
import com.android.systemui.screencapture.record.smallscreen.ui.viewmodel.RecordDetailsAppViewModel

@Composable
fun RecordDetailsAppSelector(
    viewModel: RecordDetailsAppSelectorViewModel,
    onBackPressed: () -> Unit,
    onTaskSelected: (ScreenCaptureRecentTask) -> Unit,
    modifier: Modifier = Modifier,
) {
    Column(
@@ -89,53 +87,72 @@ fun RecordDetailsAppSelector(
                color = MaterialTheme.colorScheme.onSurface,
            )
        }

        val carouselState = rememberCarouselState { viewModel.apps.size }
        HorizontalUncontainedCarousel(
            state = carouselState,
            itemWidth = 168.dp,
            itemSpacing = 24.dp,
            contentPadding = PaddingValues(horizontal = 32.dp),
            flingBehavior = CarouselDefaults.singleAdvanceFlingBehavior(carouselState),
        val tasks = viewModel.recentTasks
        val pagerState = rememberPagerState { tasks?.size ?: 1 }
        HorizontalPager(
            state = pagerState,
            contentPadding = PaddingValues(horizontal = 68.dp),
            pageSpacing = 22.dp,
            modifier = Modifier,
        ) { index ->
            val appViewModel = viewModel.apps[index]
            AppPreview(viewModel = appViewModel, modifier = Modifier)
            val task = tasks?.getOrNull(index)
            val taskViewModel =
                task?.let {
                    rememberViewModel("RecordDetailsAppSelector#taskViewModel_$index") {
                        viewModel.createTaskViewModel(task)
                    }
                }
            AppPreview(
                viewModel = taskViewModel,
                onClick = { if (task != null) onTaskSelected(task) },
            )
        }
    }
}

@Composable
private fun CarouselItemScope.AppPreview(
    viewModel: RecordDetailsAppViewModel,
private fun AppPreview(
    viewModel: RecentTaskViewModel?,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    cornersRadius: Dp = 16.dp,
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier =
            modifier
                .maskClip(RoundedCornerShape(cornersRadius))
                .clickable(onClick = viewModel.onSelect),
        modifier = modifier,
    ) {
        Icon(
            bitmap = viewModel.icon.asImageBitmap(),
        val icon = viewModel?.icon?.getOrNull()
        if (icon == null) {
            Spacer(Modifier.size(18.dp))
        } else {
            Image(
                bitmap = icon.asImageBitmap(),
                contentDescription = null,
                modifier = Modifier.size(18.dp),
            )
        }

        Card(
            onClick = onClick,
            shape = RoundedCornerShape(20.dp),
            colors =
                CardDefaults.cardColors(
                    containerColor = MaterialTheme.colorScheme.surfaceContainer
                ),
            modifier = Modifier.aspectRatio(viewModel?.thumbnail?.getOrNull().aspectRatio),
        ) {
            AnimatedContent(
            targetState = viewModel.thumbnail,
                targetState = viewModel?.thumbnail?.getOrNull(),
                contentAlignment = Alignment.Center,
                transitionSpec = { fadeIn() togetherWith fadeOut() },
            modifier =
                Modifier.clip(RoundedCornerShape(cornersRadius)).aspectRatio(9 / 16f).fillMaxSize(),
                modifier = Modifier.fillMaxSize(),
            ) { thumbnail ->
                if (thumbnail == null) {
                    Spacer(
                        modifier =
                        Modifier.background(color = MaterialTheme.colorScheme.surfaceContainerHigh)
                            Modifier.background(
                                color = MaterialTheme.colorScheme.surfaceContainerHigh
                            )
                    )
                } else {
                    Image(bitmap = thumbnail.asImageBitmap(), contentDescription = null)
@@ -143,3 +160,14 @@ private fun CarouselItemScope.AppPreview(
            }
        }
    }
}

private val Bitmap?.aspectRatio: Float
    @Composable
    get() {
        return if (this == null) {
            with(LocalResources.current.displayMetrics) { widthPixels / heightPixels.toFloat() }
        } else {
            width / height.toFloat()
        }
    }
Loading