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

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

Merge "Post recording improvements" into main

parents 8060bc41 2ac1f6f1
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
@@ -84,6 +84,9 @@ constructor(
    private val systemUIDialogFactory: SystemUIDialogFactory,
) : 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()
@@ -98,7 +101,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)
@@ -239,10 +247,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