diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/recorder/AudioRecorder.kt b/AnkiDroid/src/main/java/com/ichi2/anki/recorder/AudioRecorder.kt index 86f92c1f06b3..0fa7bb8cb3cc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/recorder/AudioRecorder.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/recorder/AudioRecorder.kt @@ -41,6 +41,7 @@ class AudioRecorder( private val context: Context, ) : Closeable { private var recorder: MediaRecorder? = null + private var isTempFile = false /** * Indicates whether the recorder is currently capturing audio. @@ -67,6 +68,7 @@ class AudioRecorder( Timber.i("AudioRecorder::startRecording (isRecording %b)", isRecording) if (isRecording) return + isTempFile = file == null val target = file ?: createTempFile() ?: return currentFile = target @@ -125,18 +127,27 @@ class AudioRecorder( /** * Stops the recording and updates state. + * @param keepFile If false, deletes the file (only if it was a temp file). */ - fun stop() { + fun stop(keepFile: Boolean = true) { if (!isRecording) return try { recorder?.stop() } catch (e: RuntimeException) { Timber.w(e, "Failed to stop recorder: likely called too soon after start") - currentFile?.delete() - currentFile = null + deleteCurrentFile() } finally { isRecording = false + + if (!keepFile && isTempFile) { + deleteCurrentFile() + } + + if (!keepFile) { + currentFile = null + } + isTempFile = false } } @@ -181,8 +192,23 @@ class AudioRecorder( * Should be called in `onDestroy()` or when the class is no longer needed. */ override fun close() { - stop() - recorder?.release() - recorder = null + try { + if (isRecording) { + // If closing while recording, we assume it's an abort + stop(keepFile = !isTempFile) + } + } finally { + recorder?.release() + recorder = null + } + } + + private fun deleteCurrentFile() { + val file = currentFile ?: return + if (file.exists() && !file.delete()) { + Timber.w("Failed to delete temporary recording file: ${file.absolutePath}") + } else { + Timber.d("Deleted temporary recording file") + } } } diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/recorder/AudioRecorderTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/recorder/AudioRecorderTest.kt index 452beae347e3..a678537ec166 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/recorder/AudioRecorderTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/recorder/AudioRecorderTest.kt @@ -36,6 +36,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config import java.io.File +import kotlin.test.assertFalse +import kotlin.test.assertNotNull import kotlin.test.junit5.JUnit5Asserter.assertNotNull @RunWith(AndroidJUnit4::class) @@ -142,4 +144,49 @@ class AudioRecorderTest : RobolectricTest() { assertEquals(customFile.absolutePath, audioRecorder.currentFile?.absolutePath) verify { mockMediaRecorder.setOutputFile(customFile.absolutePath) } } + + @Test + fun `stop should delete temp file if not kept`() { + val audioRecorder = createRecorder() + audioRecorder.start() + + val tempFile = audioRecorder.currentFile + assertTrue("Temp file should exist", tempFile?.exists() ?: false) + + audioRecorder.stop(keepFile = false) + assertFalse(tempFile?.exists() ?: true) + } + + @Test + fun `close() while recording temp file should delete the file`() { + val audioRecorder = createRecorder() + audioRecorder.start() + val tempFile = audioRecorder.currentFile + + assertTrue("Temp file should exist", tempFile?.exists() ?: false) + + audioRecorder.close() + + assertFalse(tempFile?.exists() ?: true, "Temp file should be cleaned up on close") + verify { mockMediaRecorder.release() } + } + + @Test + fun `recording after a failed stop should still work and clean up`() { + every { mockMediaRecorder.stop() } throws RuntimeException("stop failed") + + val audioRecorder = createRecorder() + audioRecorder.start() + val firstFile = audioRecorder.currentFile + + audioRecorder.stop() + + assertFalse(firstFile?.exists() ?: true, "File should be deleted if recorder.stop() fails") + assertFalse(audioRecorder.isRecording) + + // Ensure we can start again immediately + audioRecorder.start() + assertTrue(audioRecorder.isRecording) + assertNotNull(audioRecorder.currentFile) + } }