Improved Playing recording dialog

This commit is contained in:
Juha Heinanen
2025-12-05 14:01:26 +02:00
parent ed6faed318
commit 00a778a053
4 changed files with 103 additions and 8 deletions

View File

@ -99,4 +99,5 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.compose.material.icons.core)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.runtime)
}

View File

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
@ -29,18 +30,26 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.CallMade
import androidx.compose.material.icons.automirrored.filled.CallReceived
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -58,6 +67,7 @@ import com.tutpro.baresip.CallRow.Details
import com.tutpro.baresip.CustomElements.AlertDialog
import com.tutpro.baresip.CustomElements.verticalScrollbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
@ -260,10 +270,16 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
val mediaPlayer = remember { MediaPlayer() }
val scope = rememberCoroutineScope()
AlertDialog(
PlaybackDialog(
showDialog = showDialog,
title = stringResource(R.string.playing_recording),
message = "",
mediaPlayer = mediaPlayer,
onStop = {
if (mediaPlayer.isPlaying) {
mediaPlayer.stop()
}
mediaPlayer.reset()
showDialog.value = false
}
)
val isRawState = recording[0] != "" && recording[1] != ""
@ -284,13 +300,15 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
if (isRawState) {
val fileIn = File(recording[0])
val fileOut = File(recording[1])
val mergedFileName = "merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
val mergedFileName =
"merged_${fileIn.nameWithoutExtension}_${fileOut.nameWithoutExtension}.wav"
val mergedFile = File(BaresipService.filesPath + "/tmp", mergedFileName)
if (mergedFile.exists()) {
Log.d(TAG, "Using already merged file: ${mergedFile.name}")
finalFile = mergedFile
} else {
}
else {
File(BaresipService.filesPath + "/tmp").mkdirs()
if (Utils.mergeWavFiles(fileIn, fileOut, mergedFile)) {
finalFile = mergedFile
@ -315,7 +333,8 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
Log.w(TAG, "MergeWav: Failed to delete original files: ${e.message}")
}
}
} else {
}
else {
// We are already in merged state
Log.d(TAG, "Using already merged file: ${recording[0]}")
finalFile = File(recording[0])
@ -350,12 +369,14 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
Log.e(TAG, "Playback failed: $e")
Toast.makeText(ctx, "Playback error", Toast.LENGTH_SHORT).show()
}
} else {
}
else {
Toast.makeText(ctx, "Failed to process audio file", Toast.LENGTH_SHORT).show()
}
}
}
} else {
}
else {
mediaPlayer.stop()
mediaPlayer.reset()
showDialog.value = false
@ -367,3 +388,73 @@ private fun Duration(ctx: Context, detail: Details, durationText: String) {
}
}
@Composable
private fun PlaybackDialog(
showDialog: MutableState<Boolean>,
mediaPlayer: MediaPlayer,
onStop: () -> Unit
) {
if (showDialog.value) {
// State to hold progress (0.0f to 1.0f)
var currentProgress by remember { mutableFloatStateOf(0f) }
// Formatted time strings
var currentPositionText by remember { mutableStateOf("00:00") }
var totalDurationText by remember { mutableStateOf("00:00") }
// Update progress every 100ms
LaunchedEffect(showDialog.value) {
if (mediaPlayer.isPlaying) {
val duration = mediaPlayer.duration
totalDurationText = DateUtils.formatElapsedTime(duration / 1000L)
while (mediaPlayer.isPlaying) {
val current = mediaPlayer.currentPosition
currentProgress = if (duration > 0) current.toFloat() / duration.toFloat() else 0f
currentPositionText = DateUtils.formatElapsedTime(current / 1000L)
delay(100) // Poll every 100ms
}
}
}
AlertDialog(
onDismissRequest = {
// If user clicks outside, stop playback
onStop()
},
title = {
Text(text = stringResource(R.string.playing_recording))
},
text = {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(
progress = { currentProgress },
modifier = Modifier
.fillMaxWidth()
.height(8.dp),
)
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(text = currentPositionText, style = MaterialTheme.typography.bodySmall)
Text(text = totalDurationText, style = MaterialTheme.typography.bodySmall)
}
}
},
confirmButton = {
TextButton(
onClick = {
onStop()
}
) {
Text(stringResource(R.string.stop))
}
}
)
}
}

View File

@ -261,6 +261,7 @@
<string name="time">Time</string>
<string name="calls_duration">Duration</string>
<string name="playing_recording">Playing recording …</string>
<string name="stop">Stop</string>
<string name="calls_add_delete_question">Do you want to add \'%1$s\' to contacts or delete
%2$s from call history\?</string>
<string name="calls_delete_question">Do you want to delete \'%1$s\' %2$s from call history\?</string>

View File

@ -21,6 +21,7 @@ runtimeLivedata = "1.10.0"
composeMaterial3 = "1.4.0"
navigationRuntimeAndroid = "2.9.6"
composeMaterialIcons = "1.7.8"
runtime = "1.10.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
@ -47,6 +48,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat
androidx-navigation-runtime-android = { group = "androidx.navigation", name = "navigation-runtime-android", version.ref = "navigationRuntimeAndroid" }
androidx-compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core", version.ref = "composeMaterialIcons" }
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "composeMaterialIcons" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "runtime" }
[plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }