Loading packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -368,6 +368,8 @@ <string name="screen_record_capture_target_choose_app">Choose an app to record</string> <!-- Snackbar shows after the screen recording is saved [CHAR LIMIT=20] --> <string name="screen_record_video_saved">Video saved</string> <!-- Snackbar shows after the screen recording is deleted. Undo action label [CHAR LIMIT=20] --> <string name="screen_record_undo">Undo</string> <!-- Button to retake a screen recording [CHAR LIMIT=20] --> <string name="screen_record_retake">Retake</string> <!-- Button to edit a screen recording [CHAR LIMIT=20] --> Loading packages/SystemUI/res/values/styles.xml +9 −0 Original line number Diff line number Diff line Loading @@ -1054,6 +1054,15 @@ <item name="android:windowCloseOnTouchOutside">false</item> </style> <style name="ScreenCapture.PostRecord.SnackbarDialog" parent="@style/Theme.SystemUI.Dialog"> <item name="android:backgroundDimEnabled">false</item> <item name="android:showWhenLocked">true</item> <item name="android:windowBackground">@color/transparent</item> <item name="android:enforceNavigationBarContrast">false</item> <item name="android:windowIsFloating">false</item> <item name="android:windowNoTitle">true</item> </style> <style name="TextAppearance.ScreenRecord" /> <style name="TextAppearance.ScreenRecord.SwitchLabel"> Loading packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/PostRecordSnackbarDialogs.kt 0 → 100644 +160 −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.record.smallscreen.ui import android.content.Context import android.net.Uri import android.os.Bundle import android.view.Gravity import android.view.WindowManager import androidx.compose.foundation.layout.Box import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModelImpl import com.android.systemui.screencapture.record.smallscreen.ui.compose.PostRecordSnackbar import com.android.systemui.screencapture.record.smallscreen.ui.compose.SnackbarVisualsWithIcon import com.android.systemui.statusbar.phone.DialogDelegate import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create import java.io.File import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class PostRecordSnackbarDialogs @Inject constructor( @Application private val context: Context, private val dialogFactory: SystemUIDialogFactory, private val drawableViewModel: DrawableLoaderViewModelImpl, private val activityStarter: ActivityStarter, ) { fun showVideoSaved() { showSnackbar( SnackbarVisualsWithIcon( iconRes = R.drawable.ic_sync_saved_locally, message = context.getString(R.string.screen_record_video_saved), ) ) } fun showVideoDeleted(uri: Uri) { showSnackbar( visuals = SnackbarVisualsWithIcon( iconRes = R.drawable.ic_screenshot_delete, message = context.getString(R.string.screen_record_video_saved), actionLabel = context.getString(R.string.screen_record_undo), ), onActionPerformed = { activityStarter.startActivity( SmallScreenPostRecordingActivity.getStartingIntent(context, uri), true, ) }, onDismissed = { val file = uri.path?.let(::File) with(file ?: return@showSnackbar) { if (exists()) { delete() } } }, ) } private fun showSnackbar( visuals: SnackbarVisualsWithIcon, onActionPerformed: (() -> Unit)? = null, onDismissed: (() -> Unit)? = null, ) { val actionHandler = ActionHandler(onActionPerformed = onActionPerformed, onDismissed = onDismissed) dialogFactory .create( theme = R.style.ScreenCapture_PostRecord_SnackbarDialog, dialogDelegate = SnackbarDialogDelegate { actionHandler.notifyDismiss() }, ) { dialog -> val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(visuals, onActionPerformed) { when (snackbarHostState.showSnackbar(visuals)) { SnackbarResult.ActionPerformed -> actionHandler.notifyAction() SnackbarResult.Dismissed -> actionHandler.notifyDismiss() } dialog.dismissWithoutAnimation() } Box(contentAlignment = Alignment.Center) { SnackbarHost(hostState = snackbarHostState) { data -> PostRecordSnackbar(viewModel = drawableViewModel, data = data) } } } .show() } } /** Ensures that only either [onActionPerformed] or [onDismissed] is called */ private class ActionHandler( private val onActionPerformed: (() -> Unit)? = null, private val onDismissed: (() -> Unit)? = null, ) { private val notified = AtomicBoolean(false) fun notifyDismiss() { if (notified.compareAndSet(false, true)) { onDismissed?.invoke() } } fun notifyAction() { if (notified.compareAndSet(false, true)) { onActionPerformed?.invoke() } } } private class SnackbarDialogDelegate(private val onDismissed: () -> Unit) : DialogDelegate<SystemUIDialog> { override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { super.onCreate(dialog, savedInstanceState) dialog.setOnDismissListener { onDismissed() } with(dialog.window!!) { setGravity(Gravity.TOP) addFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ) addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) setWindowAnimations(-1) } } override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT override fun getHeight(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.WRAP_CONTENT } packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/SmallScreenPostRecordingActivity.kt +16 −87 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.screencapture.record.smallscreen.ui import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent Loading @@ -38,29 +41,19 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarVisuals import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.booleanResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.media3.common.MimeTypes import com.android.compose.PlatformOutlinedButton import com.android.compose.theme.PlatformTheme import com.android.systemui.lifecycle.rememberViewModel Loading @@ -78,6 +71,7 @@ class SmallScreenPostRecordingActivity constructor( private val videoPlayer: VideoPlayer, private val viewModelFactory: PostRecordingViewModel.Factory, private val postRecordSnackbarDialogs: PostRecordSnackbarDialogs, ) : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { Loading @@ -93,19 +87,7 @@ constructor( viewModelFactory.create(intent.data ?: error("Data URI is missing")) } var shouldShowSavedToast by rememberSaveable { mutableStateOf(true) } val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(shouldShowSavedToast) { if (!shouldShowSavedToast) return@LaunchedEffect shouldShowSavedToast = true snackbarHostState.showSnackbar( SnackbarVisualsWithIcon( iconRes = R.drawable.ic_sync_saved_locally, message = getString(R.string.screen_record_video_saved), ) ) } LaunchedEffect(Unit) { postRecordSnackbarDialogs.showVideoSaved() } val shouldUseFlatBottomBar = booleanResource(R.bool.screen_record_post_recording_flat_bottom_bar) Loading Loading @@ -142,7 +124,10 @@ constructor( modifier = rowModifier, ) PostRecordButton( onClick = { viewModel.delete() }, onClick = { postRecordSnackbarDialogs.showVideoDeleted(viewModel.videoUri) finish() }, drawableLoaderViewModel = viewModel, iconRes = R.drawable.ic_screenshot_delete, labelRes = R.string.screen_record_delete, Loading Loading @@ -184,63 +169,15 @@ constructor( modifier = Modifier.size(24.dp), ) } SnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.TopCenter), ) { data -> PostRecordSnackbar(viewModel = viewModel, data = data, modifier = Modifier) } } } } @Composable private fun PostRecordSnackbar( viewModel: DrawableLoaderViewModel, data: SnackbarData, modifier: Modifier = Modifier, ) { val visuals = data.visuals as? SnackbarVisualsWithIcon ?: return Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier .background( color = MaterialTheme.colorScheme.inverseSurface, shape = RoundedCornerShape(percent = 50), ) .padding(start = 12.dp, end = 20.dp) .height(48.dp), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.background( color = MaterialTheme.colorScheme.inverseOnSurface, shape = CircleShape, ) .size(24.dp), ) { LoadingIcon( icon = loadIcon(viewModel, visuals.iconRes, null).value, tint = MaterialTheme.colorScheme.inverseSurface, modifier = modifier.size(16.dp), ) } Text( text = visuals.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.inverseOnSurface, ) if (visuals.actionLabel != null) { TextButton(onClick = data::performAction) { Text( text = visuals.actionLabel, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.inverseOnSurface, ) } companion object { fun getStartingIntent(context: Context, videoUri: Uri): Intent { return Intent(context, SmallScreenPostRecordingActivity::class.java) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(videoUri, MimeTypes.VIDEO_MP4) } } } Loading Loading @@ -268,11 +205,3 @@ private fun PostRecordButton( Text(text = stringResource(labelRes), style = MaterialTheme.typography.labelLarge) } } private data class SnackbarVisualsWithIcon( override val message: String, @DrawableRes val iconRes: Int, override val actionLabel: String? = null, override val withDismissAction: Boolean = true, override val duration: SnackbarDuration = SnackbarDuration.Short, ) : SnackbarVisuals packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/compose/PostRecordSnackbar.kt 0 → 100644 +100 −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.record.smallscreen.ui.compose import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarVisuals import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.systemui.screencapture.common.ui.compose.LoadingIcon import com.android.systemui.screencapture.common.ui.compose.loadIcon import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel data class SnackbarVisualsWithIcon( override val message: String, @DrawableRes val iconRes: Int, override val actionLabel: String? = null, override val withDismissAction: Boolean = true, override val duration: SnackbarDuration = SnackbarDuration.Short, ) : SnackbarVisuals @Composable fun PostRecordSnackbar( viewModel: DrawableLoaderViewModel, data: SnackbarData, modifier: Modifier = Modifier, ) { val visuals = data.visuals as? SnackbarVisualsWithIcon ?: return Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier .background( color = MaterialTheme.colorScheme.inverseSurface, shape = RoundedCornerShape(percent = 50), ) .padding(start = 12.dp, end = 20.dp) .height(48.dp), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.background( color = MaterialTheme.colorScheme.inverseOnSurface, shape = CircleShape, ) .size(24.dp), ) { LoadingIcon( icon = loadIcon(viewModel, visuals.iconRes, null).value, tint = MaterialTheme.colorScheme.inverseSurface, modifier = modifier.size(16.dp), ) } Text( text = visuals.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.inverseOnSurface, ) if (visuals.actionLabel != null) { TextButton(onClick = data::performAction) { Text( text = visuals.actionLabel, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.inverseOnSurface, ) } } } } Loading
packages/SystemUI/res/values/strings.xml +2 −0 Original line number Diff line number Diff line Loading @@ -368,6 +368,8 @@ <string name="screen_record_capture_target_choose_app">Choose an app to record</string> <!-- Snackbar shows after the screen recording is saved [CHAR LIMIT=20] --> <string name="screen_record_video_saved">Video saved</string> <!-- Snackbar shows after the screen recording is deleted. Undo action label [CHAR LIMIT=20] --> <string name="screen_record_undo">Undo</string> <!-- Button to retake a screen recording [CHAR LIMIT=20] --> <string name="screen_record_retake">Retake</string> <!-- Button to edit a screen recording [CHAR LIMIT=20] --> Loading
packages/SystemUI/res/values/styles.xml +9 −0 Original line number Diff line number Diff line Loading @@ -1054,6 +1054,15 @@ <item name="android:windowCloseOnTouchOutside">false</item> </style> <style name="ScreenCapture.PostRecord.SnackbarDialog" parent="@style/Theme.SystemUI.Dialog"> <item name="android:backgroundDimEnabled">false</item> <item name="android:showWhenLocked">true</item> <item name="android:windowBackground">@color/transparent</item> <item name="android:enforceNavigationBarContrast">false</item> <item name="android:windowIsFloating">false</item> <item name="android:windowNoTitle">true</item> </style> <style name="TextAppearance.ScreenRecord" /> <style name="TextAppearance.ScreenRecord.SwitchLabel"> Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/PostRecordSnackbarDialogs.kt 0 → 100644 +160 −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.record.smallscreen.ui import android.content.Context import android.net.Uri import android.os.Bundle import android.view.Gravity import android.view.WindowManager import androidx.compose.foundation.layout.Box import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModelImpl import com.android.systemui.screencapture.record.smallscreen.ui.compose.PostRecordSnackbar import com.android.systemui.screencapture.record.smallscreen.ui.compose.SnackbarVisualsWithIcon import com.android.systemui.statusbar.phone.DialogDelegate import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.create import java.io.File import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject class PostRecordSnackbarDialogs @Inject constructor( @Application private val context: Context, private val dialogFactory: SystemUIDialogFactory, private val drawableViewModel: DrawableLoaderViewModelImpl, private val activityStarter: ActivityStarter, ) { fun showVideoSaved() { showSnackbar( SnackbarVisualsWithIcon( iconRes = R.drawable.ic_sync_saved_locally, message = context.getString(R.string.screen_record_video_saved), ) ) } fun showVideoDeleted(uri: Uri) { showSnackbar( visuals = SnackbarVisualsWithIcon( iconRes = R.drawable.ic_screenshot_delete, message = context.getString(R.string.screen_record_video_saved), actionLabel = context.getString(R.string.screen_record_undo), ), onActionPerformed = { activityStarter.startActivity( SmallScreenPostRecordingActivity.getStartingIntent(context, uri), true, ) }, onDismissed = { val file = uri.path?.let(::File) with(file ?: return@showSnackbar) { if (exists()) { delete() } } }, ) } private fun showSnackbar( visuals: SnackbarVisualsWithIcon, onActionPerformed: (() -> Unit)? = null, onDismissed: (() -> Unit)? = null, ) { val actionHandler = ActionHandler(onActionPerformed = onActionPerformed, onDismissed = onDismissed) dialogFactory .create( theme = R.style.ScreenCapture_PostRecord_SnackbarDialog, dialogDelegate = SnackbarDialogDelegate { actionHandler.notifyDismiss() }, ) { dialog -> val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(visuals, onActionPerformed) { when (snackbarHostState.showSnackbar(visuals)) { SnackbarResult.ActionPerformed -> actionHandler.notifyAction() SnackbarResult.Dismissed -> actionHandler.notifyDismiss() } dialog.dismissWithoutAnimation() } Box(contentAlignment = Alignment.Center) { SnackbarHost(hostState = snackbarHostState) { data -> PostRecordSnackbar(viewModel = drawableViewModel, data = data) } } } .show() } } /** Ensures that only either [onActionPerformed] or [onDismissed] is called */ private class ActionHandler( private val onActionPerformed: (() -> Unit)? = null, private val onDismissed: (() -> Unit)? = null, ) { private val notified = AtomicBoolean(false) fun notifyDismiss() { if (notified.compareAndSet(false, true)) { onDismissed?.invoke() } } fun notifyAction() { if (notified.compareAndSet(false, true)) { onActionPerformed?.invoke() } } } private class SnackbarDialogDelegate(private val onDismissed: () -> Unit) : DialogDelegate<SystemUIDialog> { override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { super.onCreate(dialog, savedInstanceState) dialog.setOnDismissListener { onDismissed() } with(dialog.window!!) { setGravity(Gravity.TOP) addFlags( WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ) addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) setWindowAnimations(-1) } } override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT override fun getHeight(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.WRAP_CONTENT }
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/SmallScreenPostRecordingActivity.kt +16 −87 Original line number Diff line number Diff line Loading @@ -16,6 +16,9 @@ package com.android.systemui.screencapture.record.smallscreen.ui import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent Loading @@ -38,29 +41,19 @@ import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarVisuals import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.booleanResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.media3.common.MimeTypes import com.android.compose.PlatformOutlinedButton import com.android.compose.theme.PlatformTheme import com.android.systemui.lifecycle.rememberViewModel Loading @@ -78,6 +71,7 @@ class SmallScreenPostRecordingActivity constructor( private val videoPlayer: VideoPlayer, private val viewModelFactory: PostRecordingViewModel.Factory, private val postRecordSnackbarDialogs: PostRecordSnackbarDialogs, ) : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { Loading @@ -93,19 +87,7 @@ constructor( viewModelFactory.create(intent.data ?: error("Data URI is missing")) } var shouldShowSavedToast by rememberSaveable { mutableStateOf(true) } val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(shouldShowSavedToast) { if (!shouldShowSavedToast) return@LaunchedEffect shouldShowSavedToast = true snackbarHostState.showSnackbar( SnackbarVisualsWithIcon( iconRes = R.drawable.ic_sync_saved_locally, message = getString(R.string.screen_record_video_saved), ) ) } LaunchedEffect(Unit) { postRecordSnackbarDialogs.showVideoSaved() } val shouldUseFlatBottomBar = booleanResource(R.bool.screen_record_post_recording_flat_bottom_bar) Loading Loading @@ -142,7 +124,10 @@ constructor( modifier = rowModifier, ) PostRecordButton( onClick = { viewModel.delete() }, onClick = { postRecordSnackbarDialogs.showVideoDeleted(viewModel.videoUri) finish() }, drawableLoaderViewModel = viewModel, iconRes = R.drawable.ic_screenshot_delete, labelRes = R.string.screen_record_delete, Loading Loading @@ -184,63 +169,15 @@ constructor( modifier = Modifier.size(24.dp), ) } SnackbarHost( hostState = snackbarHostState, modifier = Modifier.align(Alignment.TopCenter), ) { data -> PostRecordSnackbar(viewModel = viewModel, data = data, modifier = Modifier) } } } } @Composable private fun PostRecordSnackbar( viewModel: DrawableLoaderViewModel, data: SnackbarData, modifier: Modifier = Modifier, ) { val visuals = data.visuals as? SnackbarVisualsWithIcon ?: return Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier .background( color = MaterialTheme.colorScheme.inverseSurface, shape = RoundedCornerShape(percent = 50), ) .padding(start = 12.dp, end = 20.dp) .height(48.dp), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.background( color = MaterialTheme.colorScheme.inverseOnSurface, shape = CircleShape, ) .size(24.dp), ) { LoadingIcon( icon = loadIcon(viewModel, visuals.iconRes, null).value, tint = MaterialTheme.colorScheme.inverseSurface, modifier = modifier.size(16.dp), ) } Text( text = visuals.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.inverseOnSurface, ) if (visuals.actionLabel != null) { TextButton(onClick = data::performAction) { Text( text = visuals.actionLabel, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.inverseOnSurface, ) } companion object { fun getStartingIntent(context: Context, videoUri: Uri): Intent { return Intent(context, SmallScreenPostRecordingActivity::class.java) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(videoUri, MimeTypes.VIDEO_MP4) } } } Loading Loading @@ -268,11 +205,3 @@ private fun PostRecordButton( Text(text = stringResource(labelRes), style = MaterialTheme.typography.labelLarge) } } private data class SnackbarVisualsWithIcon( override val message: String, @DrawableRes val iconRes: Int, override val actionLabel: String? = null, override val withDismissAction: Boolean = true, override val duration: SnackbarDuration = SnackbarDuration.Short, ) : SnackbarVisuals
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/ui/compose/PostRecordSnackbar.kt 0 → 100644 +100 −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.record.smallscreen.ui.compose import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarData import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarVisuals import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.systemui.screencapture.common.ui.compose.LoadingIcon import com.android.systemui.screencapture.common.ui.compose.loadIcon import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel data class SnackbarVisualsWithIcon( override val message: String, @DrawableRes val iconRes: Int, override val actionLabel: String? = null, override val withDismissAction: Boolean = true, override val duration: SnackbarDuration = SnackbarDuration.Short, ) : SnackbarVisuals @Composable fun PostRecordSnackbar( viewModel: DrawableLoaderViewModel, data: SnackbarData, modifier: Modifier = Modifier, ) { val visuals = data.visuals as? SnackbarVisualsWithIcon ?: return Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = modifier .background( color = MaterialTheme.colorScheme.inverseSurface, shape = RoundedCornerShape(percent = 50), ) .padding(start = 12.dp, end = 20.dp) .height(48.dp), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier.background( color = MaterialTheme.colorScheme.inverseOnSurface, shape = CircleShape, ) .size(24.dp), ) { LoadingIcon( icon = loadIcon(viewModel, visuals.iconRes, null).value, tint = MaterialTheme.colorScheme.inverseSurface, modifier = modifier.size(16.dp), ) } Text( text = visuals.message, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.inverseOnSurface, ) if (visuals.actionLabel != null) { TextButton(onClick = data::performAction) { Text( text = visuals.actionLabel, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.inverseOnSurface, ) } } } }