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

Commit 2ac1f6f1 authored by Anton Potapov's avatar Anton Potapov
Browse files

Post recording improvements

 - Rework how the video player works. This fixes backing from another activity at a cost of it being reinitialized every time;
 - Fix snackbar clickabke area;
 - Show "Video saved" once;

Flag: com.android.systemui.new_screen_record_toolbar
Fixes: 430553811
Test: manual on foldable
Change-Id: I33da6775f8720df10a6dd12d64ca1d8ac90664db
parent 39bf2614
Loading
Loading
Loading
Loading
+12 −18
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ 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
@@ -49,25 +48,20 @@ constructor(
                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()
                        )
                ) {
            val aspectRatioModifier: Modifier =
                playerViewModel.videoAspectRatio?.let { Modifier.aspectRatio(it) } ?: Modifier
            Box(modifier = aspectRatioModifier) {
                AndroidExternalSurface(
                    onInit = {
                        onSurface { surface, _, _ ->
                                player.setSurface(surface)
                                surface.onDestroyed { player.setSurface(null) }
                            playerViewModel.onSurfaceCreated(surface)
                            surface.onDestroyed { playerViewModel.onSurfaceDestroyed() }
                        }
                    }
                )

                if (player != null) {
                    val viewModel =
                        rememberViewModel(
                            traceName = "VideoPlayer#controlsViewModel",
+42 −24
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ 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.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.isActive
@@ -51,60 +51,65 @@ constructor(
) : HydratedActivatable(), DrawableLoaderViewModel by drawableLoaderViewModelImpl {

    val videoDurationMillis: Int
        get() = mediaPlayer.duration
        get() = mediaPlayer.callSafe { duration } ?: 0

    val videoPositionMillis: Int by
        mediaPlayer
            .currentPositionFlow(10.milliseconds)
            .hydratedStateOf("VideoPlayerControlsViewModel#videoPositionMillis", 0)

    var playing: Boolean by mutableStateOf(false)
        private set
    val playing: Boolean
        get() = mediaPlayer.callSafe { isPlaying } ?: false

    var muted: Boolean by mutableStateOf(false)
        private set

    private var wasPlayingBeforeSeek: Boolean = false
    private var wasPlayingBeforeSeek: Boolean? = null

    override suspend fun onActivated() {
        coroutineScope {
            mediaPlayer
                .onComplete()
                .onEach {
                    mediaPlayer.seekTo(0)
                    playing = false
                }
                .onEach { mediaPlayer.callSafe { mediaPlayer.seekTo(0) } }
                .launchIn(this)
        }
    }

    fun updatePlaying(isPlaying: Boolean) {
        mediaPlayer.callSafe {
            if (isPlaying) {
            mediaPlayer.start()
                start()
            } else {
            mediaPlayer.pause()
                pause()
            }
        }
        this.playing = isPlaying
    }

    fun seek(positionMillis: Int) {
        mediaPlayer.callSafe {
            if (wasPlayingBeforeSeek == null) {
                wasPlayingBeforeSeek = playing
                updatePlaying(false)
        mediaPlayer.seekTo(positionMillis)
            }
            seekTo(positionMillis.toLong(), MediaPlayer.SEEK_CLOSEST)
        }
    }

    fun seekFinished() {
        updatePlaying(wasPlayingBeforeSeek)
        wasPlayingBeforeSeek?.let(::updatePlaying)
        wasPlayingBeforeSeek = null
    }

    fun updateMuted(isMuted: Boolean) {
        mediaPlayer.callSafe {
            if (isMuted) {
            mediaPlayer.setVolume(0f)
                setVolume(0f)
            } else {
            mediaPlayer.setVolume(1f)
                setVolume(1f)
            }
            muted = isMuted
        }
    }

    @AssistedFactory
    interface Factory {
@@ -124,7 +129,9 @@ private fun MediaPlayer.currentPositionFlow(pollingDelay: Duration): Flow<Int> {
        setOnSeekCompleteListener(listener)
        awaitClose { setOnSeekCompleteListener(null) }
    }
    return merge(polling, seeking).map { currentPosition }.distinctUntilChanged()
    return merge(polling, seeking)
        .mapNotNull { callSafe { currentPosition } }
        .distinctUntilChanged()
}

private fun MediaPlayer.onComplete(): Flow<Unit> = callbackFlow {
@@ -132,3 +139,14 @@ private fun MediaPlayer.onComplete(): Flow<Unit> = callbackFlow {
    setOnCompletionListener(listener)
    awaitClose { setOnCompletionListener(null) }
}

/**
 * Unfortunately [MediaPlayer] API doesn't allow to check for the current player state, so there is
 * no good way to tell if it has been released already or not before calling a method.
 */
private fun <T> MediaPlayer.callSafe(action: MediaPlayer.() -> T): T? =
    try {
        action()
    } catch (_: IllegalStateException) {
        null
    }
+17 −9
Original line number Diff line number Diff line
@@ -20,10 +20,11 @@ import android.content.Context
import android.media.AudioAttributes
import android.media.MediaPlayer
import android.net.Uri
import android.view.Surface
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.android.app.tracing.coroutines.launchTraced
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.lifecycle.HydratedActivatable
@@ -31,7 +32,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext

class VideoPlayerViewModel
@@ -45,16 +45,24 @@ constructor(
    var player: MediaPlayer? by mutableStateOf(null)
        private set

    override suspend fun onActivated() {
        coroutineScope {
            launchTraced("VideoPlayerViewModel#createPlayer") { player = createPlayer() }
    val videoAspectRatio: Float? by derivedStateOf {
        player?.run { videoWidth.toFloat() / videoHeight.toFloat() }?.takeIf { it > 0 }
    }

    val controlsViewModel: VideoPlayerControlsViewModel? = null

    suspend fun onSurfaceCreated(surface: Surface) {
        player =
            createPlayer().apply {
                setSurface(surface)
                start()
            }
    }

    override suspend fun onDeactivated() {
        player?.let {
    fun onSurfaceDestroyed() {
        player?.let { currentPlayer ->
            player = null
            it.release()
            currentPlayer.release()
        }
    }

+2 −2
Original line number Diff line number Diff line
@@ -142,7 +142,7 @@ private class SnackbarDialogDelegate(private val onDismissed: () -> Unit) :
        super.onCreate(dialog, savedInstanceState)
        dialog.setOnDismissListener { onDismissed() }
        with(dialog.window!!) {
            setGravity(Gravity.TOP)
            setGravity(Gravity.TOP or Gravity.CENTER_HORIZONTAL)
            addFlags(
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
                    WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
@@ -154,7 +154,7 @@ private class SnackbarDialogDelegate(private val onDismissed: () -> Unit) :
        }
    }

    override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.MATCH_PARENT
    override fun getWidth(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.WRAP_CONTENT

    override fun getHeight(dialog: SystemUIDialog): Int = WindowManager.LayoutParams.WRAP_CONTENT
}
+18 −2
Original line number Diff line number Diff line
@@ -74,6 +74,9 @@ constructor(
    private val postRecordSnackbarDialogs: PostRecordSnackbarDialogs,
) : ComponentActivity() {

    private val shouldShowVideoSaved: Boolean
        get() = intent.getBooleanExtra(SHOULD_SHOW_VIDEO_SAVED, SHOULD_SHOW_VIDEO_SAVED_DEFAULT)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
@@ -87,7 +90,12 @@ constructor(
                viewModelFactory.create(intent.data ?: error("Data URI is missing"))
            }

        LaunchedEffect(Unit) { postRecordSnackbarDialogs.showVideoSaved() }
        LaunchedEffect(shouldShowVideoSaved) {
            if (shouldShowVideoSaved) {
                intent.putExtra(SHOULD_SHOW_VIDEO_SAVED, false)
                postRecordSnackbarDialogs.showVideoSaved()
            }
        }

        val shouldUseFlatBottomBar =
            booleanResource(R.bool.screen_record_post_recording_flat_bottom_bar)
@@ -174,10 +182,18 @@ constructor(

    companion object {

        fun getStartingIntent(context: Context, videoUri: Uri): Intent {
        private const val SHOULD_SHOW_VIDEO_SAVED = "should_show_video_saved"
        private const val SHOULD_SHOW_VIDEO_SAVED_DEFAULT = false

        fun getStartingIntent(
            context: Context,
            videoUri: Uri,
            shouldShowVideoSaved: Boolean = SHOULD_SHOW_VIDEO_SAVED_DEFAULT,
        ): 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)
                .putExtra(SHOULD_SHOW_VIDEO_SAVED, shouldShowVideoSaved)
        }
    }
}
Loading