fix(android): make top audio strip controls functional
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-10 21:03:03 +03:00
parent 58b554731d
commit 27fba86915

View File

@@ -317,6 +317,8 @@ fun ChatScreen(
var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) } var dismissedPinnedMessageId by remember { mutableStateOf<Long?>(null) }
var topAudioStrip by remember { mutableStateOf<TopAudioStrip?>(null) } var topAudioStrip by remember { mutableStateOf<TopAudioStrip?>(null) }
var forceStopAudioSourceId by remember { mutableStateOf<String?>(null) } var forceStopAudioSourceId by remember { mutableStateOf<String?>(null) }
var forceToggleAudioSourceId by remember { mutableStateOf<String?>(null) }
var forceCycleSpeedAudioSourceId by remember { mutableStateOf<String?>(null) }
var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) } var actionMenuMessage by remember { mutableStateOf<MessageItem?>(null) }
var pendingDeleteForAll by remember { mutableStateOf(false) } var pendingDeleteForAll by remember { mutableStateOf(false) }
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
@@ -662,12 +664,15 @@ fun ChatScreen(
Surface( Surface(
shape = CircleShape, shape = CircleShape,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
modifier = Modifier.size(30.dp), modifier = Modifier
.size(30.dp)
.clip(CircleShape)
.clickable { forceToggleAudioSourceId = strip.sourceId },
) { ) {
Box(contentAlignment = Alignment.Center) { Box(contentAlignment = Alignment.Center) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = if (strip.isPlaying) Icons.Filled.Pause else Icons.Filled.PlayArrow,
contentDescription = "Play top audio", contentDescription = if (strip.isPlaying) "Pause top audio" else "Play top audio",
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
) )
} }
@@ -688,9 +693,12 @@ fun ChatScreen(
Surface( Surface(
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f), color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.clickable { forceCycleSpeedAudioSourceId = strip.sourceId },
) { ) {
Text( Text(
text = "1x", text = strip.speedLabel,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
) )
@@ -698,6 +706,8 @@ fun ChatScreen(
IconButton( IconButton(
onClick = { onClick = {
forceStopAudioSourceId = strip.sourceId forceStopAudioSourceId = strip.sourceId
forceToggleAudioSourceId = null
forceCycleSpeedAudioSourceId = null
topAudioStrip = null topAudioStrip = null
}, },
) { ) {
@@ -762,11 +772,23 @@ fun ChatScreen(
} }
}, },
forceStopAudioSourceId = forceStopAudioSourceId, forceStopAudioSourceId = forceStopAudioSourceId,
forceToggleAudioSourceId = forceToggleAudioSourceId,
forceCycleSpeedAudioSourceId = forceCycleSpeedAudioSourceId,
onForceStopAudioSourceHandled = { sourceId -> onForceStopAudioSourceHandled = { sourceId ->
if (forceStopAudioSourceId == sourceId) { if (forceStopAudioSourceId == sourceId) {
forceStopAudioSourceId = null forceStopAudioSourceId = null
} }
}, },
onForceToggleAudioSourceHandled = { sourceId ->
if (forceToggleAudioSourceId == sourceId) {
forceToggleAudioSourceId = null
}
},
onForceCycleSpeedAudioSourceHandled = { sourceId ->
if (forceCycleSpeedAudioSourceId == sourceId) {
forceCycleSpeedAudioSourceId = null
}
},
onClick = { onClick = {
if (state.actionState.mode == MessageSelectionMode.MULTI) { if (state.actionState.mode == MessageSelectionMode.MULTI) {
onToggleMessageMultiSelection(message) onToggleMessageMultiSelection(message)
@@ -1789,7 +1811,11 @@ private fun MessageBubble(
onAttachmentVideoClick: (String) -> Unit, onAttachmentVideoClick: (String) -> Unit,
onAudioPlaybackChanged: (TopAudioStrip) -> Unit, onAudioPlaybackChanged: (TopAudioStrip) -> Unit,
forceStopAudioSourceId: String?, forceStopAudioSourceId: String?,
forceToggleAudioSourceId: String?,
forceCycleSpeedAudioSourceId: String?,
onForceStopAudioSourceHandled: (String) -> Unit, onForceStopAudioSourceHandled: (String) -> Unit,
onForceToggleAudioSourceHandled: (String) -> Unit,
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
onClick: () -> Unit, onClick: () -> Unit,
onLongPress: () -> Unit, onLongPress: () -> Unit,
) { ) {
@@ -2009,7 +2035,11 @@ private fun MessageBubble(
messageId = message.id, messageId = message.id,
onPlaybackChanged = onAudioPlaybackChanged, onPlaybackChanged = onAudioPlaybackChanged,
forceStopAudioSourceId = forceStopAudioSourceId, forceStopAudioSourceId = forceStopAudioSourceId,
forceToggleAudioSourceId = forceToggleAudioSourceId,
forceCycleSpeedAudioSourceId = forceCycleSpeedAudioSourceId,
onForceStopAudioSourceHandled = onForceStopAudioSourceHandled, onForceStopAudioSourceHandled = onForceStopAudioSourceHandled,
onForceToggleAudioSourceHandled = onForceToggleAudioSourceHandled,
onForceCycleSpeedAudioSourceHandled = onForceCycleSpeedAudioSourceHandled,
) )
} }
@@ -2484,7 +2514,11 @@ private fun AudioAttachmentPlayer(
messageId: Long, messageId: Long,
onPlaybackChanged: (TopAudioStrip) -> Unit, onPlaybackChanged: (TopAudioStrip) -> Unit,
forceStopAudioSourceId: String?, forceStopAudioSourceId: String?,
forceToggleAudioSourceId: String?,
forceCycleSpeedAudioSourceId: String?,
onForceStopAudioSourceHandled: (String) -> Unit, onForceStopAudioSourceHandled: (String) -> Unit,
onForceToggleAudioSourceHandled: (String) -> Unit,
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
) { ) {
var isPlaying by remember(url) { mutableStateOf(false) } var isPlaying by remember(url) { mutableStateOf(false) }
var isPrepared by remember(url) { mutableStateOf(false) } var isPrepared by remember(url) { mutableStateOf(false) }
@@ -2494,6 +2528,18 @@ private fun AudioAttachmentPlayer(
var speedIndex by remember(url) { mutableStateOf(0) } var speedIndex by remember(url) { mutableStateOf(0) }
var isSeeking by remember(url) { mutableStateOf(false) } var isSeeking by remember(url) { mutableStateOf(false) }
var seekFraction by remember(url) { mutableStateOf(0f) } var seekFraction by remember(url) { mutableStateOf(0f) }
fun emitTopStrip(playing: Boolean) {
onPlaybackChanged(
TopAudioStrip(
messageId = messageId,
sourceId = "player:$url",
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = playing,
speedLabel = formatAudioSpeed(speedOptions[speedIndex]),
)
)
}
val mediaPlayer = remember(url) { val mediaPlayer = remember(url) {
MediaPlayer().apply { MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
@@ -2511,15 +2557,7 @@ private fun AudioAttachmentPlayer(
runCatching { player.seekTo(0) } runCatching { player.seekTo(0) }
positionMs = 0 positionMs = 0
AppAudioFocusCoordinator.release("player:$url") AppAudioFocusCoordinator.release("player:$url")
onPlaybackChanged( emitTopStrip(false)
TopAudioStrip(
messageId = messageId,
sourceId = "player:$url",
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = false,
)
)
} }
setDataSource(url) setDataSource(url)
prepareAsync() prepareAsync()
@@ -2531,15 +2569,7 @@ private fun AudioAttachmentPlayer(
if (activeId != null && activeId != currentId && isPlaying) { if (activeId != null && activeId != currentId && isPlaying) {
runCatching { mediaPlayer.pause() } runCatching { mediaPlayer.pause() }
isPlaying = false isPlaying = false
onPlaybackChanged( emitTopStrip(false)
TopAudioStrip(
messageId = messageId,
sourceId = currentId,
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = false,
)
)
} }
} }
} }
@@ -2551,17 +2581,48 @@ private fun AudioAttachmentPlayer(
positionMs = 0 positionMs = 0
isPlaying = false isPlaying = false
AppAudioFocusCoordinator.release(currentId) AppAudioFocusCoordinator.release(currentId)
onPlaybackChanged( emitTopStrip(false)
TopAudioStrip(
messageId = messageId,
sourceId = currentId,
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = false,
)
)
onForceStopAudioSourceHandled(currentId) onForceStopAudioSourceHandled(currentId)
} }
LaunchedEffect(forceToggleAudioSourceId) {
val currentId = "player:$url"
if (forceToggleAudioSourceId != currentId) return@LaunchedEffect
if (!isPrepared) {
onForceToggleAudioSourceHandled(currentId)
return@LaunchedEffect
}
if (isPlaying) {
runCatching { mediaPlayer.pause() }
isPlaying = false
AppAudioFocusCoordinator.release(currentId)
emitTopStrip(false)
} else {
if (durationMs > 0 && positionMs >= durationMs - 200) {
runCatching { mediaPlayer.seekTo(0) }
positionMs = 0
}
runCatching {
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
}
AppAudioFocusCoordinator.request(currentId)
runCatching { mediaPlayer.start() }
isPlaying = true
emitTopStrip(true)
}
onForceToggleAudioSourceHandled(currentId)
}
LaunchedEffect(forceCycleSpeedAudioSourceId) {
val currentId = "player:$url"
if (forceCycleSpeedAudioSourceId != currentId) return@LaunchedEffect
speedIndex = (speedIndex + 1) % speedOptions.size
if (isPrepared) {
runCatching {
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
}
}
emitTopStrip(isPlaying)
onForceCycleSpeedAudioSourceHandled(currentId)
}
LaunchedEffect(isPlaying, isPrepared, isSeeking) { LaunchedEffect(isPlaying, isPrepared, isSeeking) {
if (!isPlaying || !isPrepared || isSeeking) return@LaunchedEffect if (!isPlaying || !isPrepared || isSeeking) return@LaunchedEffect
while (isPlaying) { while (isPlaying) {
@@ -2582,15 +2643,7 @@ private fun AudioAttachmentPlayer(
mediaPlayer.stop() mediaPlayer.stop()
} }
AppAudioFocusCoordinator.release("player:$url") AppAudioFocusCoordinator.release("player:$url")
onPlaybackChanged( emitTopStrip(false)
TopAudioStrip(
messageId = messageId,
sourceId = "player:$url",
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = false,
)
)
mediaPlayer.release() mediaPlayer.release()
} }
} }
@@ -2612,15 +2665,7 @@ private fun AudioAttachmentPlayer(
mediaPlayer.pause() mediaPlayer.pause()
isPlaying = false isPlaying = false
AppAudioFocusCoordinator.release("player:$url") AppAudioFocusCoordinator.release("player:$url")
onPlaybackChanged( emitTopStrip(false)
TopAudioStrip(
messageId = messageId,
sourceId = "player:$url",
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = false,
)
)
} else { } else {
if (durationMs > 0 && positionMs >= durationMs - 200) { if (durationMs > 0 && positionMs >= durationMs - 200) {
runCatching { mediaPlayer.seekTo(0) } runCatching { mediaPlayer.seekTo(0) }
@@ -2632,15 +2677,7 @@ private fun AudioAttachmentPlayer(
AppAudioFocusCoordinator.request("player:$url") AppAudioFocusCoordinator.request("player:$url")
mediaPlayer.start() mediaPlayer.start()
isPlaying = true isPlaying = true
onPlaybackChanged( emitTopStrip(true)
TopAudioStrip(
messageId = messageId,
sourceId = "player:$url",
title = playbackTitle,
subtitle = playbackSubtitle,
isPlaying = true,
)
)
} }
}, },
enabled = isPrepared, enabled = isPrepared,
@@ -2659,10 +2696,11 @@ private fun AudioAttachmentPlayer(
mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex]) mediaPlayer.playbackParams = mediaPlayer.playbackParams.setSpeed(speedOptions[speedIndex])
} }
} }
emitTopStrip(isPlaying)
}, },
enabled = isPrepared, enabled = isPrepared,
) { ) {
Text("${speedOptions[speedIndex]}x") Text(formatAudioSpeed(speedOptions[speedIndex]))
} }
} }
if (isVoice) { if (isVoice) {
@@ -2794,6 +2832,14 @@ private fun formatDuration(ms: Int): String {
return "$min:${sec.toString().padStart(2, '0')}" return "$min:${sec.toString().padStart(2, '0')}"
} }
private fun formatAudioSpeed(speed: Float): String {
return if (speed % 1f == 0f) {
"${speed.toInt()}x"
} else {
"${speed}x"
}
}
private fun resolveRemoteAudioDurationMs(url: String): Int? { private fun resolveRemoteAudioDurationMs(url: String): Int? {
return runCatching { return runCatching {
val retriever = MediaMetadataRetriever() val retriever = MediaMetadataRetriever()
@@ -3008,7 +3054,11 @@ private fun ChatInfoTabContent(
messageId = entry.sourceMessageId ?: entry.title.hashCode().toLong(), messageId = entry.sourceMessageId ?: entry.title.hashCode().toLong(),
onPlaybackChanged = {}, onPlaybackChanged = {},
forceStopAudioSourceId = null, forceStopAudioSourceId = null,
forceToggleAudioSourceId = null,
forceCycleSpeedAudioSourceId = null,
onForceStopAudioSourceHandled = {}, onForceStopAudioSourceHandled = {},
onForceToggleAudioSourceHandled = {},
onForceCycleSpeedAudioSourceHandled = {},
) )
} }
} }
@@ -3152,4 +3202,5 @@ private data class TopAudioStrip(
val title: String, val title: String,
val subtitle: String, val subtitle: String,
val isPlaying: Boolean, val isPlaying: Boolean,
val speedLabel: String = "1x",
) )