diff --git a/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt b/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt index 48523f8..5790690 100644 --- a/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt +++ b/app/src/main/java/com/isolaatti/audio/common/components/AudioPlayer.kt @@ -1,6 +1,8 @@ package com.isolaatti.audio.common.components import androidx.annotation.OptIn +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons @@ -13,6 +15,11 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp @@ -32,9 +39,10 @@ fun AudioPlayer( isLoading: Boolean, durationMs: Long, onPlay: () -> Unit = {}, - onPause: () -> Unit = {} + onPause: () -> Unit = {}, + onSeek: (position: Float) -> Unit = {} ) { - val sliderValue = positionMs.coerceAtLeast(1).toFloat() / durationMs.coerceAtLeast(1).toFloat() + @@ -51,7 +59,27 @@ fun AudioPlayer( if(isLoading) { LinearProgressIndicator(modifier = Modifier.padding(horizontal = 4.dp).weight(1f)) } else { - Slider(value = sliderValue, onValueChange = {}, modifier = Modifier.padding(horizontal = 4.dp).weight(1f)) + var value by remember { mutableFloatStateOf(0f) } + val interactionSource = remember { MutableInteractionSource() } + val isDragged by interactionSource.collectIsDraggedAsState() + LaunchedEffect(positionMs) { + if(!isDragged) { + value = positionMs.toFloat() + } + + } + + Slider( + value = value, + onValueChange = { + value = it + }, + onValueChangeFinished = { + onSeek(value) + }, + modifier = Modifier.padding(horizontal = 4.dp).weight(1f), + valueRange = 0f..durationMs.toFloat() + ) } Text("${positionMs.milliseconds.clockFormat()}/${durationMs.milliseconds.clockFormat()}") diff --git a/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt b/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt index 09b56a3..d7d37a8 100644 --- a/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt +++ b/app/src/main/java/com/isolaatti/audio/common/components/AudioRecorder.kt @@ -56,6 +56,7 @@ fun AudioRecorder( isLoading: Boolean, durationSeconds: Long, position: Long, + onSeek: (position: Float) -> Unit = {}, modifier: Modifier = Modifier ) { @@ -67,9 +68,9 @@ fun AudioRecorder( var duration by remember { mutableLongStateOf(0L) } - LaunchedEffect(isPlaying) { + LaunchedEffect(isRecording, recordIsPaused) { coroutineScope { - while(isPlaying) { + while(isRecording && !recordIsPaused) { duration += 500 delay(500.milliseconds) } @@ -93,7 +94,7 @@ fun AudioRecorder( when { audioRecordPermissionState.status.isGranted -> { when { - !isRecording -> { + !isRecording && !recordIsStopped-> { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center @@ -112,7 +113,7 @@ fun AudioRecorder( } } - isRecording -> { + isRecording && !recordIsStopped -> { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 8.dp) @@ -121,9 +122,10 @@ fun AudioRecorder( Spacer(Modifier.weight(1f)) IconButton(onClick = { if(recordIsPaused) { - onPauseRecording() - } else { onStartRecording(true) + + } else { + onPauseRecording() } }) { @@ -141,7 +143,7 @@ fun AudioRecorder( } } - recordIsStopped -> { + else -> { AudioPlayer( modifier = Modifier.fillMaxWidth(), positionMs = position, @@ -149,7 +151,8 @@ fun AudioRecorder( isPlaying = isPlaying, durationMs = durationSeconds, onPlay = onPlay, - onPause = onPause + onPause = onPause, + onSeek = onSeek ) } } diff --git a/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt b/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt index c7a5640..149be0c 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/presentation/CreatePostViewModel.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.io.File import javax.inject.Inject @HiltViewModel @@ -68,6 +69,12 @@ class CreatePostViewModel @Inject constructor( var ended: Boolean = false // endregion + // region Recorder state + var isRecording: MutableStateFlow = MutableStateFlow(false) + var recordingIsPaused: MutableStateFlow = MutableStateFlow(false) + var recodingIsStopped: MutableStateFlow = MutableStateFlow(false) + // endregion + /** * postDiscussion() and editDiscussion() will check for audios pending to upload (drafts). It will diff --git a/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt b/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt index 0dbf439..9d01269 100644 --- a/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt +++ b/app/src/main/java/com/isolaatti/posting/posts/ui/CreatePostActivity.kt @@ -67,9 +67,11 @@ import com.isolaatti.posting.posts.components.PostAttachments import com.isolaatti.posting.posts.domain.PostingSteps import com.isolaatti.posting.posts.presentation.CreatePostViewModel import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.guava.await import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.util.Calendar import kotlin.time.Duration.Companion.milliseconds @@ -144,6 +146,10 @@ class CreatePostActivity : IsolaattiBaseActivity() { } } + private lateinit var recordOutputFile: File + + + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -152,13 +158,27 @@ class CreatePostActivity : IsolaattiBaseActivity() { val sessionToken = SessionToken(this, ComponentName(this, MediaService::class.java)) mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync() - mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this) else MediaRecorder() lifecycleScope.launch { mediaController = mediaControllerFuture.await() mediaController.addListener(playerListener) + // check audios directory + val directory = File(this@CreatePostActivity.filesDir, "recordings") + + withContext(Dispatchers.IO) { + if(!directory.isDirectory) { + if(directory.isFile) { + directory.delete() + } + directory.mkdir() + } + } + + recordOutputFile = File(directory, "${System.currentTimeMillis()}.3gp") + + setContent { val text by viewModel.liveContent.observeAsState("") val focusRequester = remember { FocusRequester() } @@ -257,6 +277,7 @@ class CreatePostActivity : IsolaattiBaseActivity() { IconButton( onClick = { audioRecorderIsVisible = !audioRecorderIsVisible + mediaRecorder = if (Build.VERSION.SDK_INT >= 31) MediaRecorder(this@CreatePostActivity) else MediaRecorder() } ) { Icon(painterResource(id = R.drawable.baseline_mic_24), null) @@ -275,7 +296,9 @@ class CreatePostActivity : IsolaattiBaseActivity() { val duration by viewModel.duration.collectAsState() val isLoading by viewModel.isLoading.collectAsState() - val outputFile = File(this@CreatePostActivity.filesDir, "${System.currentTimeMillis()}.3gp") + val isRecording by viewModel.isRecording.collectAsState() + val recordIsPaused by viewModel.recordingIsPaused.collectAsState() + val recordIsStopped by viewModel.recodingIsStopped.collectAsState() LaunchedEffect(isPlaying) { while(isPlaying) { @@ -288,6 +311,29 @@ class CreatePostActivity : IsolaattiBaseActivity() { modifier = Modifier.padding(16.dp), onDismiss = { audioRecorderIsVisible = false + + // stop playback + mediaController.stop() + mediaController.clearMediaItems() + + // delete file + lifecycleScope.launch(Dispatchers.IO) { + if(recordOutputFile.exists()) { + recordOutputFile.delete() + } + } + + // release media recorder + mediaRecorder.release() + + // clear references + viewModel.recodingIsStopped.value = false + viewModel.isRecording.value = false + viewModel.recordingIsPaused.value = false + viewModel.isPlaying.value = false + viewModel.duration.value = 0L + viewModel.position.value = 0L + viewModel.audioToUpload = null }, onPlay = { if(mediaController.isCommandAvailable(Player.COMMAND_PREPARE)) { @@ -302,28 +348,41 @@ class CreatePostActivity : IsolaattiBaseActivity() { onPause = { mediaController.pause() }, - onStartRecording = { + onStartRecording = { fromPause -> - mediaRecorder.run { - setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) - setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) - if (Build.VERSION.SDK_INT >= 26) { - setOutputFile(outputFile) - } else { - setOutputFile(outputFile.path) + if(fromPause) { + mediaRecorder.resume() + } else { + mediaRecorder.run { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + if (Build.VERSION.SDK_INT >= 26) { + setOutputFile(recordOutputFile) + } else { + setOutputFile(recordOutputFile.path) + } + + prepare() + start() } - - prepare() - start() } + viewModel.recodingIsStopped.value = false + viewModel.isRecording.value = true + viewModel.recordingIsPaused.value = false + + + }, onPauseRecording = { mediaRecorder.pause() + viewModel.recodingIsStopped.value = false + viewModel.isRecording.value = true + viewModel.recordingIsPaused.value = true }, onStopRecording = { mediaRecorder.stop() - val audioUri = outputFile.toUri() + val audioUri = recordOutputFile.toUri() mediaController.setMediaItem( MediaItem.Builder() .setUri(audioUri) @@ -333,14 +392,20 @@ class CreatePostActivity : IsolaattiBaseActivity() { .build() ) viewModel.audioToUpload = audioUri + viewModel.recodingIsStopped.value = true + viewModel.isRecording.value = false + viewModel.recordingIsPaused.value = false }, isPlaying = isPlaying, position = position, durationSeconds = duration, - isRecording = false, - recordIsPaused = false, - recordIsStopped = false, - isLoading = isLoading + isRecording = isRecording, + recordIsPaused = recordIsPaused, + recordIsStopped = recordIsStopped, + isLoading = isLoading, + onSeek = { ms -> + mediaController.seekTo(ms.toLong()) + } ) }