Loading packages/SystemUI/AndroidManifest.xml +0 −2 Original line number Diff line number Diff line Loading @@ -547,9 +547,7 @@ <activity android:name=".screencapture.record.smallscreen.ui.SmallScreenPostRecordingActivity" android:excludeFromRecents="true" android:exported="false" android:finishOnCloseSystemDialogs="true" android:showForAllUsers="true" android:theme="@style/ScreenCapture.PostRecord" /> Loading packages/SystemUI/res/values/strings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -364,6 +364,15 @@ <string name="screen_record_edit">Edit</string> <!-- Button to delete a screen recording [CHAR LIMIT=20] --> <string name="screen_record_delete">Delete</string> <!-- Button to play record preview video [CHAR LIMIT=20] --> <string name="screen_record_play">Play</string> <!-- Button to pause record preview video [CHAR LIMIT=20] --> <string name="screen_record_pause">Pause</string> <!-- Button to mute record preview video [CHAR LIMIT=20] --> <string name="screen_record_mute">Mute</string> <!-- Button to unmute record preview video [CHAR LIMIT=20] --> <string name="screen_record_unmute">Unmute</string> <string name="screen_record_video_preview_elapsed_time_template" translatable="false">%s / %s</string> <!-- Button text for taking a fullscreen screenshot [CHAR LIMIT=50] --> <string name="screen_capture_fullscreen_screenshot_button">Take screenshot of entire screen</string> Loading packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/compose/DefaultVideoPlayerControls.kt 0 → 100644 +185 −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.player.ui.compose import android.text.format.DateUtils import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.res.R 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 import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerControlsViewModel import kotlin.math.roundToInt @Composable fun DefaultVideoPlayerControls( viewModel: VideoPlayerControlsViewModel, modifier: Modifier = Modifier, color: Color = Color.White, contrastColor: Color = Color.Black.copy(alpha = 0.5f), ) { val backgroundBrush = Brush.verticalGradient(colors = listOf(Color.Transparent, contrastColor)) Column( verticalArrangement = Arrangement.Bottom, modifier = modifier.heightIn(min = 164.dp).drawBehind { drawRect(backgroundBrush) }, ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth(), ) { PlayerButton( viewModel = viewModel, checked = viewModel.playing, onCheckedChanged = { viewModel.updatePlaying(it) }, iconRes = if (viewModel.playing) { R.drawable.ic_media_pause_button } else { R.drawable.ic_media_play_button }, labelRes = if (viewModel.playing) { R.string.screen_record_pause } else { R.string.screen_record_play }, color = color, ) ElapsedTimeText( currentPositionMillis = viewModel.videoPositionMillis, durationMillis = viewModel.videoDurationMillis, color = color, ) PlayerButton( viewModel = viewModel, checked = viewModel.muted, onCheckedChanged = { viewModel.updateMuted(it) }, iconRes = if (viewModel.muted) { R.drawable.ic_speaker_mute } else { R.drawable.ic_speaker_on }, labelRes = if (viewModel.muted) { R.string.screen_record_mute } else { R.string.screen_record_unmute }, color = color, ) } val animatedValue by animateFloatAsState(viewModel.videoPositionMillis.toFloat()) Slider( value = animatedValue, valueRange = 0f..viewModel.videoDurationMillis.toFloat(), onValueChange = { viewModel.seek(it.roundToInt()) }, onValueChangeFinished = { viewModel.seekFinished() }, colors = SliderDefaults.colors( activeTrackColor = color, inactiveTrackColor = color.copy(alpha = 0.38f), inactiveTickColor = color, thumbColor = color, ), modifier = Modifier.padding(16.dp), ) } } @Composable private fun PlayerButton( viewModel: DrawableLoaderViewModel, checked: Boolean, onCheckedChanged: (Boolean) -> Unit, @DrawableRes iconRes: Int, @StringRes labelRes: Int, color: Color, modifier: Modifier = Modifier, ) { TextButton( onClick = { onCheckedChanged(!checked) }, colors = ButtonDefaults.textButtonColors(contentColor = color), modifier = modifier.size(48.dp), ) { LoadingIcon( icon = loadIcon( viewModel = viewModel, resId = iconRes, contentDescription = ContentDescription.Loaded(stringResource(labelRes)), ) .value, modifier = Modifier.size(24.dp), ) } } @Composable private fun ElapsedTimeText( currentPositionMillis: Int, durationMillis: Int, color: Color, modifier: Modifier = Modifier, ) { val elapsedTime = format(currentPositionMillis) val duration = format(durationMillis) Text( text = stringResource( R.string.screen_record_video_preview_elapsed_time_template, elapsedTime, duration, ), style = MaterialTheme.typography.labelMedium.copy(), textAlign = TextAlign.Center, color = color, modifier = modifier, ) } private fun format(durationMillis: Int): String { return DateUtils.formatElapsedTime(durationMillis / DateUtils.SECOND_IN_MILLIS) } packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/compose/VideoPlayer.kt 0 → 100644 +83 −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.player.ui.compose import android.net.Uri import androidx.compose.foundation.AndroidExternalSurface import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerControlsViewModel import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerViewModel import javax.inject.Inject class VideoPlayer @Inject constructor( private val videoPlayerViewModelFactory: VideoPlayerViewModel.Factory, private val videoPlayerControlsViewModelFactory: VideoPlayerControlsViewModel.Factory, ) { @Composable fun Content( uri: Uri, modifier: Modifier = Modifier, videoControls: @Composable BoxScope.(vm: VideoPlayerControlsViewModel) -> Unit = { vm -> DefaultVideoPlayerControls(vm, Modifier.align(Alignment.BottomCenter)) }, ) { val playerViewModel = rememberViewModel(traceName = "VideoPlayer#viewModel", key = uri) { videoPlayerViewModelFactory.create(uri) } val player = playerViewModel.player if (player == null) { Spacer(modifier = modifier) } else { Box(contentAlignment = Alignment.Center, modifier = modifier) { Box( modifier = Modifier.aspectRatio( player.videoWidth.toFloat() / player.videoHeight.toFloat() ) ) { AndroidExternalSurface( onInit = { onSurface { surface, _, _ -> player.setSurface(surface) surface.onDestroyed { player.setSurface(null) } } } ) val viewModel = rememberViewModel( traceName = "VideoPlayer#controlsViewModel", key = player, ) { videoPlayerControlsViewModelFactory.create(player) } videoControls(viewModel) } } } } } packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/viewmodel/VideoPlayerControlsViewModel.kt 0 → 100644 +134 −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.player.ui.viewmodel import android.media.MediaPlayer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.kairos.awaitClose import com.android.systemui.lifecycle.HydratedActivatable import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModelImpl import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive class VideoPlayerControlsViewModel @AssistedInject constructor( @Assisted private val mediaPlayer: MediaPlayer, private val drawableLoaderViewModelImpl: DrawableLoaderViewModelImpl, ) : HydratedActivatable(), DrawableLoaderViewModel by drawableLoaderViewModelImpl { val videoDurationMillis: Int get() = mediaPlayer.duration val videoPositionMillis: Int by mediaPlayer .currentPositionFlow(10.milliseconds) .hydratedStateOf("VideoPlayerControlsViewModel#videoPositionMillis", 0) var playing: Boolean by mutableStateOf(false) private set var muted: Boolean by mutableStateOf(false) private set private var wasPlayingBeforeSeek: Boolean = false override suspend fun onActivated() { coroutineScope { mediaPlayer .onComplete() .onEach { mediaPlayer.seekTo(0) playing = false } .launchIn(this) } } fun updatePlaying(isPlaying: Boolean) { if (isPlaying) { mediaPlayer.start() } else { mediaPlayer.pause() } this.playing = isPlaying } fun seek(positionMillis: Int) { wasPlayingBeforeSeek = playing updatePlaying(false) mediaPlayer.seekTo(positionMillis) } fun seekFinished() { updatePlaying(wasPlayingBeforeSeek) } fun updateMuted(isMuted: Boolean) { if (isMuted) { mediaPlayer.setVolume(0f) } else { mediaPlayer.setVolume(1f) } muted = isMuted } @AssistedFactory interface Factory { fun create(player: MediaPlayer): VideoPlayerControlsViewModel } } private fun MediaPlayer.currentPositionFlow(pollingDelay: Duration): Flow<Int> { val polling = flow { while (currentCoroutineContext().isActive) { delay(pollingDelay) emit(Unit) } } val seeking = conflatedCallbackFlow { val listener = MediaPlayer.OnSeekCompleteListener { trySend(Unit) } setOnSeekCompleteListener(listener) awaitClose { setOnSeekCompleteListener(null) } } return merge(polling, seeking).map { currentPosition }.distinctUntilChanged() } private fun MediaPlayer.onComplete(): Flow<Unit> = callbackFlow { val listener = MediaPlayer.OnCompletionListener { trySend(Unit) } setOnCompletionListener(listener) awaitClose { setOnCompletionListener(null) } } Loading
packages/SystemUI/AndroidManifest.xml +0 −2 Original line number Diff line number Diff line Loading @@ -547,9 +547,7 @@ <activity android:name=".screencapture.record.smallscreen.ui.SmallScreenPostRecordingActivity" android:excludeFromRecents="true" android:exported="false" android:finishOnCloseSystemDialogs="true" android:showForAllUsers="true" android:theme="@style/ScreenCapture.PostRecord" /> Loading
packages/SystemUI/res/values/strings.xml +9 −0 Original line number Diff line number Diff line Loading @@ -364,6 +364,15 @@ <string name="screen_record_edit">Edit</string> <!-- Button to delete a screen recording [CHAR LIMIT=20] --> <string name="screen_record_delete">Delete</string> <!-- Button to play record preview video [CHAR LIMIT=20] --> <string name="screen_record_play">Play</string> <!-- Button to pause record preview video [CHAR LIMIT=20] --> <string name="screen_record_pause">Pause</string> <!-- Button to mute record preview video [CHAR LIMIT=20] --> <string name="screen_record_mute">Mute</string> <!-- Button to unmute record preview video [CHAR LIMIT=20] --> <string name="screen_record_unmute">Unmute</string> <string name="screen_record_video_preview_elapsed_time_template" translatable="false">%s / %s</string> <!-- Button text for taking a fullscreen screenshot [CHAR LIMIT=50] --> <string name="screen_capture_fullscreen_screenshot_button">Take screenshot of entire screen</string> Loading
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/compose/DefaultVideoPlayerControls.kt 0 → 100644 +185 −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.player.ui.compose import android.text.format.DateUtils import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.drawBehind import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.res.R 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 import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerControlsViewModel import kotlin.math.roundToInt @Composable fun DefaultVideoPlayerControls( viewModel: VideoPlayerControlsViewModel, modifier: Modifier = Modifier, color: Color = Color.White, contrastColor: Color = Color.Black.copy(alpha = 0.5f), ) { val backgroundBrush = Brush.verticalGradient(colors = listOf(Color.Transparent, contrastColor)) Column( verticalArrangement = Arrangement.Bottom, modifier = modifier.heightIn(min = 164.dp).drawBehind { drawRect(backgroundBrush) }, ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.padding(horizontal = 4.dp).fillMaxWidth(), ) { PlayerButton( viewModel = viewModel, checked = viewModel.playing, onCheckedChanged = { viewModel.updatePlaying(it) }, iconRes = if (viewModel.playing) { R.drawable.ic_media_pause_button } else { R.drawable.ic_media_play_button }, labelRes = if (viewModel.playing) { R.string.screen_record_pause } else { R.string.screen_record_play }, color = color, ) ElapsedTimeText( currentPositionMillis = viewModel.videoPositionMillis, durationMillis = viewModel.videoDurationMillis, color = color, ) PlayerButton( viewModel = viewModel, checked = viewModel.muted, onCheckedChanged = { viewModel.updateMuted(it) }, iconRes = if (viewModel.muted) { R.drawable.ic_speaker_mute } else { R.drawable.ic_speaker_on }, labelRes = if (viewModel.muted) { R.string.screen_record_mute } else { R.string.screen_record_unmute }, color = color, ) } val animatedValue by animateFloatAsState(viewModel.videoPositionMillis.toFloat()) Slider( value = animatedValue, valueRange = 0f..viewModel.videoDurationMillis.toFloat(), onValueChange = { viewModel.seek(it.roundToInt()) }, onValueChangeFinished = { viewModel.seekFinished() }, colors = SliderDefaults.colors( activeTrackColor = color, inactiveTrackColor = color.copy(alpha = 0.38f), inactiveTickColor = color, thumbColor = color, ), modifier = Modifier.padding(16.dp), ) } } @Composable private fun PlayerButton( viewModel: DrawableLoaderViewModel, checked: Boolean, onCheckedChanged: (Boolean) -> Unit, @DrawableRes iconRes: Int, @StringRes labelRes: Int, color: Color, modifier: Modifier = Modifier, ) { TextButton( onClick = { onCheckedChanged(!checked) }, colors = ButtonDefaults.textButtonColors(contentColor = color), modifier = modifier.size(48.dp), ) { LoadingIcon( icon = loadIcon( viewModel = viewModel, resId = iconRes, contentDescription = ContentDescription.Loaded(stringResource(labelRes)), ) .value, modifier = Modifier.size(24.dp), ) } } @Composable private fun ElapsedTimeText( currentPositionMillis: Int, durationMillis: Int, color: Color, modifier: Modifier = Modifier, ) { val elapsedTime = format(currentPositionMillis) val duration = format(durationMillis) Text( text = stringResource( R.string.screen_record_video_preview_elapsed_time_template, elapsedTime, duration, ), style = MaterialTheme.typography.labelMedium.copy(), textAlign = TextAlign.Center, color = color, modifier = modifier, ) } private fun format(durationMillis: Int): String { return DateUtils.formatElapsedTime(durationMillis / DateUtils.SECOND_IN_MILLIS) }
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/compose/VideoPlayer.kt 0 → 100644 +83 −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.player.ui.compose import android.net.Uri import androidx.compose.foundation.AndroidExternalSurface import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerControlsViewModel import com.android.systemui.screencapture.record.smallscreen.player.ui.viewmodel.VideoPlayerViewModel import javax.inject.Inject class VideoPlayer @Inject constructor( private val videoPlayerViewModelFactory: VideoPlayerViewModel.Factory, private val videoPlayerControlsViewModelFactory: VideoPlayerControlsViewModel.Factory, ) { @Composable fun Content( uri: Uri, modifier: Modifier = Modifier, videoControls: @Composable BoxScope.(vm: VideoPlayerControlsViewModel) -> Unit = { vm -> DefaultVideoPlayerControls(vm, Modifier.align(Alignment.BottomCenter)) }, ) { val playerViewModel = rememberViewModel(traceName = "VideoPlayer#viewModel", key = uri) { videoPlayerViewModelFactory.create(uri) } val player = playerViewModel.player if (player == null) { Spacer(modifier = modifier) } else { Box(contentAlignment = Alignment.Center, modifier = modifier) { Box( modifier = Modifier.aspectRatio( player.videoWidth.toFloat() / player.videoHeight.toFloat() ) ) { AndroidExternalSurface( onInit = { onSurface { surface, _, _ -> player.setSurface(surface) surface.onDestroyed { player.setSurface(null) } } } ) val viewModel = rememberViewModel( traceName = "VideoPlayer#controlsViewModel", key = player, ) { videoPlayerControlsViewModelFactory.create(player) } videoControls(viewModel) } } } } }
packages/SystemUI/src/com/android/systemui/screencapture/record/smallscreen/player/ui/viewmodel/VideoPlayerControlsViewModel.kt 0 → 100644 +134 −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.player.ui.viewmodel import android.media.MediaPlayer import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.android.systemui.kairos.awaitClose import com.android.systemui.lifecycle.HydratedActivatable import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModel import com.android.systemui.screencapture.common.ui.viewmodel.DrawableLoaderViewModelImpl import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive class VideoPlayerControlsViewModel @AssistedInject constructor( @Assisted private val mediaPlayer: MediaPlayer, private val drawableLoaderViewModelImpl: DrawableLoaderViewModelImpl, ) : HydratedActivatable(), DrawableLoaderViewModel by drawableLoaderViewModelImpl { val videoDurationMillis: Int get() = mediaPlayer.duration val videoPositionMillis: Int by mediaPlayer .currentPositionFlow(10.milliseconds) .hydratedStateOf("VideoPlayerControlsViewModel#videoPositionMillis", 0) var playing: Boolean by mutableStateOf(false) private set var muted: Boolean by mutableStateOf(false) private set private var wasPlayingBeforeSeek: Boolean = false override suspend fun onActivated() { coroutineScope { mediaPlayer .onComplete() .onEach { mediaPlayer.seekTo(0) playing = false } .launchIn(this) } } fun updatePlaying(isPlaying: Boolean) { if (isPlaying) { mediaPlayer.start() } else { mediaPlayer.pause() } this.playing = isPlaying } fun seek(positionMillis: Int) { wasPlayingBeforeSeek = playing updatePlaying(false) mediaPlayer.seekTo(positionMillis) } fun seekFinished() { updatePlaying(wasPlayingBeforeSeek) } fun updateMuted(isMuted: Boolean) { if (isMuted) { mediaPlayer.setVolume(0f) } else { mediaPlayer.setVolume(1f) } muted = isMuted } @AssistedFactory interface Factory { fun create(player: MediaPlayer): VideoPlayerControlsViewModel } } private fun MediaPlayer.currentPositionFlow(pollingDelay: Duration): Flow<Int> { val polling = flow { while (currentCoroutineContext().isActive) { delay(pollingDelay) emit(Unit) } } val seeking = conflatedCallbackFlow { val listener = MediaPlayer.OnSeekCompleteListener { trySend(Unit) } setOnSeekCompleteListener(listener) awaitClose { setOnSeekCompleteListener(null) } } return merge(polling, seeking).map { currentPosition }.distinctUntilChanged() } private fun MediaPlayer.onComplete(): Flow<Unit> = callbackFlow { val listener = MediaPlayer.OnCompletionListener { trySend(Unit) } setOnCompletionListener(listener) awaitClose { setOnCompletionListener(null) } }