feat: keep screen awake during fullscreen video and show media controls

Prevent the device from sleeping during fullscreen video playback with
FLAG_KEEP_SCREEN_ON. Add a MediaSessionService so Android shows media
controls in the notification shade and lock screen when the app is
backgrounded, covering both fullscreen and PiP video modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Barry Deen
2026-04-05 14:38:07 -04:00
co-authored by Claude Opus 4.6
parent 7648e5a5b8
commit 77148d127f
7 changed files with 86 additions and 0 deletions
+1
View File
@@ -88,6 +88,7 @@ dependencies {
implementation(libs.media3.exoplayer.hls)
implementation(libs.media3.datasource.okhttp)
implementation(libs.media3.ui)
implementation(libs.media3.session)
implementation(libs.biometric)
implementation(libs.splashscreen)
implementation(libs.profileinstaller)
+10
View File
@@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
@@ -41,6 +43,14 @@
<data android:scheme="nostr" />
</intent-filter>
</activity>
<service
android:name=".ui.component.VideoPlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent-filter>
</service>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -8,6 +8,7 @@ import android.content.Context
import android.content.ContextWrapper
import android.net.Uri
import android.view.Gravity
import android.view.WindowManager
import android.view.ViewGroup.LayoutParams
import android.widget.FrameLayout
import android.widget.ImageButton
@@ -159,11 +160,15 @@ fun FullScreenVideoPlayer(
insetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
insetsController.hide(WindowInsetsCompat.Type.systemBars())
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
VideoMediaSession.attach(activity, exoPlayer)
dialog.show()
onDispose {
if (!minimizedToPip) VideoMediaSession.release()
if (ownsPlayer) exoPlayer.release()
if (dialog.isShowing) dialog.dismiss()
}
@@ -43,6 +43,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import android.view.TextureView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
@@ -72,6 +73,7 @@ object PipController {
fun exitPip() {
val state = pipState.value ?: return
VideoMediaSession.release()
state.player.release()
pipState.value = null
activeVideoUrl.compareAndSet(state.url, null)
@@ -93,6 +95,8 @@ fun FloatingVideoPlayer(
val state by PipController.pipState.collectAsState()
val currentState = state
val context = LocalContext.current
AnimatedVisibility(
visible = currentState != null,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
@@ -102,6 +106,11 @@ fun FloatingVideoPlayer(
val pipState = currentState ?: return@AnimatedVisibility
val isMuted by PipController.globalMuted.collectAsState()
DisposableEffect(pipState.player) {
VideoMediaSession.attach(context, pipState.player)
onDispose {}
}
LaunchedEffect(isMuted) {
pipState.player.volume = if (isMuted) 0f else 1f
}
@@ -0,0 +1,30 @@
package com.wisp.app.ui.component
import android.content.Context
import android.content.Intent
import androidx.media3.common.Player
import androidx.media3.session.MediaSession
object VideoMediaSession {
internal var session: MediaSession? = null
private set
private var appContext: Context? = null
fun attach(context: Context, player: Player) {
if (session?.player === player) return
val ctx = context.applicationContext
release()
appContext = ctx
session = MediaSession.Builder(ctx, player).build()
ctx.startService(Intent(ctx, VideoPlaybackService::class.java))
}
fun release() {
session?.release()
session = null
appContext?.let {
it.stopService(Intent(it, VideoPlaybackService::class.java))
}
appContext = null
}
}
@@ -0,0 +1,30 @@
package com.wisp.app.ui.component
import android.content.Intent
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
class VideoPlaybackService : MediaSessionService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
VideoMediaSession.session?.let { addSession(it) }
return START_NOT_STICKY
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
return VideoMediaSession.session
}
override fun onTaskRemoved(rootIntent: Intent?) {
val session = VideoMediaSession.session
if (session == null || !session.player.playWhenReady) {
stopSelf()
}
}
override fun onDestroy() {
VideoMediaSession.session?.let { removeSession(it) }
super.onDestroy()
}
}
+1
View File
@@ -51,6 +51,7 @@ media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", versi
media3-exoplayer-hls = { group = "androidx.media3", name = "media3-exoplayer-hls", version.ref = "media3" }
media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "media3" }
media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" }
media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "media3" }
biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" }
profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" }
splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" }