feat: Use internal directory for saving files

This commit is contained in:
Myzel394 2023-10-26 18:27:10 +02:00
parent 517516518f
commit 9dc1c05d69
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
9 changed files with 148 additions and 129 deletions

View File

@ -2,7 +2,6 @@ package app.myzel394.alibi.db
import android.media.MediaRecorder
import android.os.Build
import android.util.Log
import app.myzel394.alibi.R
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
@ -63,7 +62,7 @@ data class AppSettings(
}
@Serializable
data class LastRecording(
data class RecordingInformation(
val folderPath: String,
@Serializable(with = LocalDateTimeSerializer::class)
val recordingStart: LocalDateTime,
@ -72,91 +71,8 @@ data class LastRecording(
val fileExtension: String,
val forceExactMaxDuration: Boolean,
) {
val fileFolder: File
get() = File(folderPath)
val filePaths: List<File>
get() =
File(folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension
name.toIntOrNull() != null
}?.toList() ?: emptyList()
val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()
private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile = File("$folderPath/${outputFile.nameWithoutExtension}-raw.${fileExtension}")
outputFile.renameTo(rawFile)
val command = "-sseof ${maxDuration / -1000} -i $rawFile -y $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)
throw Exception("Failed to strip concatenated audio")
}
}
suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recordingStart
.format(ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("$fileFolder/$fileName.${fileExtension}")
if (outputFile.exists() && !forceConcatenation) {
return outputFile
}
val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recordingStart.format(ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${intervalDuration}'" +
" -metadata max_duration='${maxDuration}'" +
" $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.getState(),
session.getReturnCode(),
session.getFailStackTrace()
)
)
throw Exception("Failed to concatenate audios")
}
val minRequiredForPossibleInExactMaxDuration = maxDuration / intervalDuration
if (forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}
return outputFile
}
val hasRecordingsAvailable
get() = File(folderPath).listFiles()?.isNotEmpty() ?: false
}
@Serializable

View File

@ -0,0 +1,119 @@
package app.myzel394.alibi.helpers
import android.content.Context
import android.util.Log
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.ui.RECORDER_SUBFOLDER_NAME
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode
import java.io.File
import java.time.format.DateTimeFormatter
data class AudioRecorderExporter(
val recording: RecordingInformation,
) {
val filePaths: List<File>
get() =
File(recording.folderPath).listFiles()?.filter {
val name = it.nameWithoutExtension
name.toIntOrNull() != null
}?.toList() ?: emptyList()
val hasRecordingAvailable: Boolean
get() = filePaths.isNotEmpty()
private fun stripConcatenatedFileToExactDuration(
outputFile: File
) {
// Move the concatenated file to a temporary file
val rawFile =
File("${recording.folderPath}/${outputFile.nameWithoutExtension}-raw.${recording.fileExtension}")
outputFile.renameTo(rawFile)
val command = "-sseof ${recording.maxDuration / -1000} -i $rawFile -y $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)
throw Exception("Failed to strip concatenated audio")
}
}
suspend fun concatenateFiles(forceConcatenation: Boolean = false): File {
val paths = filePaths.joinToString("|")
val fileName = recording.recordingStart
.format(DateTimeFormatter.ISO_DATE_TIME)
.toString()
.replace(":", "-")
.replace(".", "_")
val outputFile = File("${recording.folderPath}/$fileName.${recording.fileExtension}")
if (outputFile.exists() && !forceConcatenation) {
return outputFile
}
val command = "-i 'concat:$paths' -y" +
" -acodec copy" +
" -metadata title='$fileName' " +
" -metadata date='${recording.recordingStart.format(DateTimeFormatter.ISO_DATE_TIME)}'" +
" -metadata batch_count='${filePaths.size}'" +
" -metadata batch_duration='${recording.intervalDuration}'" +
" -metadata max_duration='${recording.maxDuration}'" +
" $outputFile"
val session = FFmpegKit.execute(command)
if (!ReturnCode.isSuccess(session.returnCode)) {
Log.d(
"Audio Concatenation",
String.format(
"Command failed with state %s and rc %s.%s",
session.state,
session.returnCode,
session.failStackTrace,
)
)
throw Exception("Failed to concatenate audios")
}
val minRequiredForPossibleInExactMaxDuration =
recording.maxDuration / recording.intervalDuration
if (recording.forceExactMaxDuration && filePaths.size > minRequiredForPossibleInExactMaxDuration) {
stripConcatenatedFileToExactDuration(outputFile)
}
return outputFile
}
suspend fun cleanupFiles() {
filePaths.forEach {
runCatching {
it.delete()
}
}
}
companion object {
fun getFolder(context: Context) = File(context.filesDir, RECORDER_SUBFOLDER_NAME)
fun clearAllRecordings(context: Context) {
getFolder(context).deleteRecursively()
}
fun hasRecordingsAvailable(context: Context) {
getFolder(context).listFiles()?.isNotEmpty() ?: false
}
}
}

View File

@ -5,7 +5,7 @@ import android.media.MediaRecorder.OnErrorListener
import android.os.Build
import java.lang.IllegalStateException
class AudioRecorderService: IntervalRecorderService() {
class AudioRecorderService : IntervalRecorderService() {
var amplitudesAmount = 1000
var recorder: MediaRecorder? = null
@ -13,7 +13,7 @@ class AudioRecorderService: IntervalRecorderService() {
var onError: () -> Unit = {}
val filePath: String
get() = "$folder/$counter.${settings!!.fileExtension}"
get() = "${outputFolder}/$counter.${settings!!.fileExtension}"
private fun createRecorder(): MediaRecorder {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {

View File

@ -1,39 +1,37 @@
package app.myzel394.alibi.services
import android.content.Context
import android.media.MediaRecorder
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AudioRecorderSettings
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.helpers.AudioRecorderExporter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.File
import java.time.LocalDateTime
import java.util.Timer
import java.util.TimerTask
import java.util.UUID
import java.util.concurrent.Executor
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
abstract class IntervalRecorderService: ExtraRecorderInformationService() {
abstract class IntervalRecorderService : ExtraRecorderInformationService() {
private var job = SupervisorJob()
private var scope = CoroutineScope(Dispatchers.IO + job)
protected var counter = 0
private set
protected lateinit var folder: File
var settings: Settings? = null
protected set
private lateinit var cycleTimer: ScheduledExecutorService
fun createLastRecording(): LastRecording = LastRecording(
folderPath = folder.absolutePath,
protected val outputFolder: File
get() = AudioRecorderExporter.getFolder(this)
fun createLastRecording(): RecordingInformation = RecordingInformation(
folderPath = outputFolder.absolutePath,
recordingStart = recordingStart,
maxDuration = settings!!.maxDuration,
fileExtension = settings!!.fileExtension,
@ -60,18 +58,10 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
}
}
private fun getRandomFileFolder(): String {
// uuid
val folder = UUID.randomUUID().toString()
return "${externalCacheDir!!.absolutePath}/$folder"
}
override fun start() {
super.start()
folder = File(getRandomFileFolder())
folder.mkdirs()
outputFolder.mkdirs()
scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
@ -104,7 +94,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val timeMultiplier = settings!!.maxDuration / settings!!.intervalDuration
val earliestCounter = counter - timeMultiplier
folder.listFiles()?.forEach { file ->
outputFolder.listFiles()?.forEach { file ->
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return
if (fileCounter < earliestCounter) {
@ -123,7 +113,7 @@ abstract class IntervalRecorderService: ExtraRecorderInformationService() {
val encoder: Int,
) {
val fileExtension: String
get() = when(outputFormat) {
get() = when (outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"

View File

@ -2,10 +2,12 @@ package app.myzel394.alibi.ui
import android.os.Build
import androidx.compose.ui.unit.dp
import java.io.File
val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val MAX_AMPLITUDE = 20000
val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val RECORDER_SUBFOLDER_NAME = ".recordings"
// You are not allowed to change the constants below.
// If you do so, you will be blocked on GitHub.

View File

@ -1,6 +1,5 @@
package app.myzel394.alibi.ui
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@ -13,10 +12,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
@ -24,7 +19,6 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.ui.enums.Screen
import app.myzel394.alibi.ui.models.AudioRecorderModel
import app.myzel394.alibi.ui.screens.AboutScreen

View File

@ -42,6 +42,7 @@ import app.myzel394.alibi.NotificationHelper
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE
import app.myzel394.alibi.ui.components.atoms.PermissionRequester
@ -79,6 +80,9 @@ fun StartRecording(
it
)
}
AudioRecorderExporter.clearAllRecordings(context)
audioRecorder.startRecording(context)
}
}
@ -144,7 +148,7 @@ fun StartRecording(
.fillMaxWidth(),
textAlign = TextAlign.Center,
)
if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingAvailable) {
if (audioRecorder.lastRecording != null && audioRecorder.lastRecording!!.hasRecordingsAvailable) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,

View File

@ -4,22 +4,17 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.media.MediaRecorder
import android.os.IBinder
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModel
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.services.AudioRecorderService
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.services.RecorderService
import kotlinx.coroutines.flow.last
import kotlinx.serialization.json.Json
class AudioRecorderModel : ViewModel() {
@ -44,7 +39,7 @@ class AudioRecorderModel : ViewModel() {
var recorderService: AudioRecorderService? = null
private set
var lastRecording: LastRecording? by mutableStateOf<LastRecording?>(null)
var lastRecording: RecordingInformation? by mutableStateOf<RecordingInformation?>(null)
private set
var onRecordingSave: () -> Unit = {}

View File

@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.RecordingStatus
import app.myzel394.alibi.ui.components.AudioRecorder.molecules.StartRecording
@ -37,8 +36,7 @@ import app.myzel394.alibi.ui.utils.rememberFileSaverDialog
import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.db.LastRecording
import app.myzel394.alibi.services.RecorderNotificationHelper
import app.myzel394.alibi.helpers.AudioRecorderExporter
import app.myzel394.alibi.ui.effects.rememberSettings
import app.myzel394.alibi.ui.models.AudioRecorderModel
import kotlinx.coroutines.delay
@ -67,7 +65,8 @@ fun AudioRecorder(
delay(100)
try {
val file = audioRecorder.lastRecording!!.concatenateFiles()
val file =
AudioRecorderExporter(audioRecorder.lastRecording!!).concatenateFiles()
saveFile(file, file.name)
} catch (error: Exception) {