Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RecorderThread: Record to internal storage first #72

Merged
merged 3 commits into from
Jun 6, 2022
Merged
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
6 changes: 2 additions & 4 deletions app/src/main/java/com/chiller3/bcr/RecorderInCallService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -183,16 +183,14 @@ class RecorderInCallService : InCallService(), RecorderThread.OnRecordingComplet
}

override fun onRecordingCompleted(thread: RecorderThread, uri: Uri) {
val decoded = Uri.decode(uri.toString())
Log.i(TAG, "Recording completed: ${thread.id}: ${thread.redact(decoded)}")
Log.i(TAG, "Recording completed: ${thread.id}: ${thread.redact(uri)}")
handler.post {
onThreadExited()
}
}

override fun onRecordingFailed(thread: RecorderThread, errorMsg: String?, uri: Uri?) {
val decoded = Uri.decode(uri.toString())
Log.w(TAG, "Recording failed: ${thread.id}: ${thread.redact(decoded)}")
Log.w(TAG, "Recording failed: ${thread.id}: ${uri?.let { thread.redact(it) }}")
handler.post {
onThreadExited()

Expand Down
177 changes: 121 additions & 56 deletions app/src/main/java/com/chiller3/bcr/RecorderThread.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import android.net.Uri
import android.os.Build
import android.os.ParcelFileDescriptor
import android.system.Os
import android.system.OsConstants
import android.telecom.Call
import android.telecom.PhoneAccount
import android.util.Log
Expand All @@ -18,6 +17,8 @@ import com.chiller3.bcr.format.Encoder
import com.chiller3.bcr.format.Format
import com.chiller3.bcr.format.Formats
import com.chiller3.bcr.format.SampleRates
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.time.Instant
Expand Down Expand Up @@ -73,7 +74,7 @@ class RecorderThread(
formatParam = savedFormat.second
}

fun redact(msg: String): String {
private fun redact(msg: String): String {
synchronized(filenameLock) {
var result = msg

Expand All @@ -85,6 +86,8 @@ class RecorderThread(
}
}

fun redact(uri: Uri): String = redact(Uri.decode(uri.toString()))

/**
* Update [filename] with information from [details].
*
Expand Down Expand Up @@ -152,22 +155,27 @@ class RecorderThread(
Log.i(tag, "Recording cancelled before it began")
} else {
val initialFilename = synchronized(filenameLock) { filename }
val outputFile = createFileInDefaultDir(initialFilename, format.mimeTypeContainer)
resultUri = outputFile.uri

val (file, pfd) = openOutputFile(initialFilename, format.mimeTypeContainer)
resultUri = file.uri

pfd.use {
recordUntilCancelled(it)
}

val finalFilename = synchronized(filenameLock) { filename }
if (finalFilename != initialFilename) {
Log.i(tag, "Renaming ${redact(initialFilename)} to ${redact(finalFilename)}")
try {
openFile(outputFile).use {
recordUntilCancelled(it)
}
} finally {
val finalFilename = synchronized(filenameLock) { filename }
if (finalFilename != initialFilename) {
Log.i(tag, "Renaming ${redact(initialFilename)} to ${redact(finalFilename)}")

if (outputFile.renameTo(finalFilename)) {
resultUri = outputFile.uri
} else {
Log.w(tag, "Failed to rename to final filename: ${redact(finalFilename)}")
}
}

if (file.renameTo(finalFilename)) {
resultUri = file.uri
} else {
Log.w(tag, "Failed to rename to final filename: ${redact(finalFilename)}")
tryMoveToUserDir(outputFile)?.let {
resultUri = it.uri
}
}

Expand Down Expand Up @@ -210,66 +218,123 @@ class RecorderThread(
}

private fun dumpLogcat() {
openOutputFile("${filename}.log", "text/plain").pfd.use {
Os.lseek(it.fileDescriptor, 0, OsConstants.SEEK_END)
val outputFile = createFileInDefaultDir("${filename}.log", "text/plain")

val process = ProcessBuilder("logcat", "-d").start()
try {
val data = process.inputStream.use { stream -> stream.readBytes() }
Os.write(it.fileDescriptor, data, 0, data.size)
} finally {
process.waitFor()
try {
openFile(outputFile).use {
val process = ProcessBuilder("logcat", "-d").start()
try {
val data = process.inputStream.use { stream -> stream.readBytes() }
Os.write(it.fileDescriptor, data, 0, data.size)
} finally {
process.waitFor()
}
}
} finally {
tryMoveToUserDir(outputFile)
}
}

data class OutputFile(val file: DocumentFile, val pfd: ParcelFileDescriptor)
/**
* Try to move [sourceFile] to the user output directory.
*
* @return Whether the user output directory is set and the file was successfully moved
*/
private fun tryMoveToUserDir(sourceFile: DocumentFile): DocumentFile? {
val userDir = Preferences.getSavedOutputDir(context)?.let {
// Only returns null on API <21
DocumentFile.fromTreeUri(context, it)!!
} ?: return null

val redactedSource = redact(sourceFile.uri)

return try {
val targetFile = moveFileToDir(sourceFile, userDir)
val redactedTarget = redact(targetFile.uri)

Log.i(tag, "Successfully moved $redactedSource to $redactedTarget")
sourceFile.delete()

targetFile
} catch (e: Exception) {
Log.e(tag, "Failed to move $redactedSource to $userDir", e)
null
}
}

/**
* Try to create and open a new output file in the user-chosen directory if possible and fall
* back to the default output directory if not. [name] should not contain a file extension. The
* file extension is automatically determined from [mimeType].
* Move [sourceFile] to [targetDir].
*
* @throws IOException if the file could not be created in either directory
* @return The [DocumentFile] for the newly moved file.
*/
private fun openOutputFile(name: String, mimeType: String): OutputFile {
val userUri = Preferences.getSavedOutputDir(context)
if (userUri != null) {
try {
// Only returns null on API <21
val userDir = DocumentFile.fromTreeUri(context, userUri)!!
Log.d(tag, "Using user-specified directory: ${userDir.uri}")
private fun moveFileToDir(sourceFile: DocumentFile, targetDir: DocumentFile): DocumentFile {
val targetFile = createFileInDir(targetDir, sourceFile.name!!, sourceFile.type!!)

return openOutputFileInDir(userDir, name, mimeType)
} catch (e: Exception) {
Log.e(tag, "Failed to open file in user-specified directory: $userUri", e)
try {
openFile(sourceFile).use { sourcePfd ->
FileInputStream(sourcePfd.fileDescriptor).use { sourceStream ->
openFile(targetFile).use { targetPfd ->
FileOutputStream(targetPfd.fileDescriptor).use { targetStream ->
val sourceChannel = sourceStream.channel
val targetChannel = targetStream.channel

var offset = 0L
var remain = sourceChannel.size()

while (remain > 0) {
val n = targetChannel.transferFrom(sourceChannel, offset, remain)
offset += n
remain -= n
}
}
}
}
}

sourceFile.delete()
return targetFile
} catch (e: Exception) {
targetFile.delete()
throw e
}
}

val fallbackDir = DocumentFile.fromFile(Preferences.getDefaultOutputDir(context))
Log.d(tag, "Using fallback directory: ${fallbackDir.uri}")
/**
* Create [name] in the default output directory.
*
* @param name Should not contain a file extension
* @param mimeType Determines the file extension
*
* @throws IOException if the file could not be created in the default directory
*/
private fun createFileInDefaultDir(name: String, mimeType: String): DocumentFile {
val defaultDir = DocumentFile.fromFile(Preferences.getDefaultOutputDir(context))
return createFileInDir(defaultDir, name, mimeType)
}

return openOutputFileInDir(fallbackDir, name, mimeType)
/**
* Create a new file with name [name] inside [dir].
*
* @param name Should not contain a file extension
* @param mimeType Determines the file extension
*
* @throws IOException if file creation fails
*/
private fun createFileInDir(dir: DocumentFile, name: String, mimeType: String): DocumentFile {
Log.d(tag, "Creating ${redact(name)} with MIME type $mimeType in ${dir.uri}")

return dir.createFile(mimeType, name)
?: throw IOException("Failed to create file in ${dir.uri}")
}

/**
* Create and open a new output file with name [name] inside [directory]. [name] should not
* contain a file extension. The extension is determined [mimeType]. The file extension is
* automatically determined from [format].
* Open seekable file descriptor to [file].
*
* @throws IOException if file creation or opening fails
* @throws IOException if [file] cannot be opened
*/
private fun openOutputFileInDir(
directory: DocumentFile,
name: String,
mimeType: String,
): OutputFile {
val file = directory.createFile(mimeType, name)
?: throw IOException("Failed to create file in ${directory.uri}")
val pfd = context.contentResolver.openFileDescriptor(file.uri, "rw")
private fun openFile(file: DocumentFile): ParcelFileDescriptor =
context.contentResolver.openFileDescriptor(file.uri, "rw")
?: throw IOException("Failed to open file at ${file.uri}")
return OutputFile(file, pfd)
}

/**
* Record from [MediaRecorder.AudioSource.VOICE_CALL] until [cancel] is called or an audio
Expand Down