Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions AnkiDroid/src/main/java/com/ichi2/anki/recorder/AudioRecorder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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
}
}

Expand Down Expand Up @@ -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")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Loading