From 00a778a0536349ddd092c18b09c18d8a483c857f Mon Sep 17 00:00:00 2001 From: Juha Heinanen Date: Fri, 5 Dec 2025 14:01:26 +0200 Subject: [PATCH] Improved Playing recording dialog --- app/build.gradle.kts | 1 + .../com/tutpro/baresip/CallDetailsScreen.kt | 107 ++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + gradle/libs.versions.toml | 2 + 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d13acd71..91bca146 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } diff --git a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt index 32c66dc8..0b1f0da1 100644 --- a/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt +++ b/app/src/main/kotlin/com/tutpro/baresip/CallDetailsScreen.kt @@ -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, + 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)) + } + } + ) + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 118b1949..fffd893f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -261,6 +261,7 @@ Time Duration Playing recording … + Stop Do you want to add \'%1$s\' to contacts or delete %2$s from call history\? Do you want to delete \'%1$s\' %2$s from call history\? diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 064bb8cf..8fe70555 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }