feat: Add basic recording saving functionality

This commit is contained in:
Myzel394 2023-08-05 15:16:25 +02:00
parent 35c59754b5
commit c8a02567ab
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
9 changed files with 454 additions and 132 deletions

View File

@ -9,6 +9,7 @@ android {
compileSdk 33 compileSdk 33
defaultConfig { defaultConfig {
multiDexEnabled true
applicationId "app.myzel394.locationtest" applicationId "app.myzel394.locationtest"
minSdk 24 minSdk 24
targetSdk 33 targetSdk 33

View File

@ -106,7 +106,7 @@ data class AudioRecorderSettings(
} }
fun setMaxDuration(duration: Long): AudioRecorderSettings { fun setMaxDuration(duration: Long): AudioRecorderSettings {
if (duration < 60 * 1000L || duration > 60 * 60 * 1000L) { if (duration < 60 * 1000L || duration > 24 * 60 * 60 * 1000L) {
throw Exception("Max duration must be between 1 minute and 1 hour") throw Exception("Max duration must be between 1 minute and 1 hour")
} }

View File

@ -3,6 +3,7 @@ package app.myzel394.locationtest.services
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection
import android.media.MediaRecorder import android.media.MediaRecorder
import android.os.Binder import android.os.Binder
import android.os.Build import android.os.Build
@ -12,21 +13,30 @@ import android.os.Looper
import android.util.Log import android.util.Log
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import app.myzel394.locationtest.R import app.myzel394.locationtest.R
import app.myzel394.locationtest.dataStore
import app.myzel394.locationtest.db.AudioRecorderSettings
import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.ReturnCode import com.arthenica.ffmpegkit.ReturnCode
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.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date import java.util.Date
import java.util.UUID; import java.util.UUID;
const val INTERVAL_DURATION = 10000L
class RecorderService: Service() { class RecorderService: Service() {
private val binder = LocalBinder() private val binder = LocalBinder()
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private var mediaRecorder: MediaRecorder? = null private var mediaRecorder: MediaRecorder? = null
private var onError: MediaRecorder.OnErrorListener? = null private var onError: MediaRecorder.OnErrorListener? = null
@ -34,27 +44,30 @@ class RecorderService: Service() {
private var counter = 0 private var counter = 0
lateinit var settings: Settings
var recordingStart = mutableStateOf<LocalDateTime?>(null) var recordingStart = mutableStateOf<LocalDateTime?>(null)
private set private set
var fileFolder: String? = null var fileFolder: String? = null
private set private set
var bitRate: Int? = null
private set
var recordingState: RecorderState = RecorderState.IDLE var recordingState: RecorderState = RecorderState.IDLE
private set private set
val isRecording: Boolean val isRecording: Boolean
get() = recordingStart.value != null get() = recordingStart.value != null
val filePaths = mutableListOf<String>()
var originalRecordingStart: LocalDateTime? = null
private set
override fun onBind(p0: Intent?): IBinder = binder override fun onBind(p0: Intent?): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) { when (intent?.action) {
Actions.START.toString() -> { Actions.START.toString() -> {
val fileFolder = intent.getStringExtra("fileFolder") fileFolder = getRandomFileFolder(this)
val bitRate = intent.getIntExtra("bitRate", 320000)
start(fileFolder, bitRate) start()
} }
Actions.STOP.toString() -> stop() Actions.STOP.toString() -> stop()
} }
@ -62,6 +75,15 @@ class RecorderService: Service() {
return super.onStartCommand(intent, flags, startId) return super.onStartCommand(intent, flags, startId)
} }
val progress: Float
get() {
val start = recordingStart.value ?: return 0f
val now = LocalDateTime.now()
val duration = now.toEpochSecond(ZoneId.systemDefault().rules.getOffset(now)) - start.toEpochSecond(ZoneId.systemDefault().rules.getOffset(start))
return duration / (settings.maxDuration / 1000f)
}
fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) { fun setOnErrorListener(onError: MediaRecorder.OnErrorListener) {
this.onError = onError this.onError = onError
} }
@ -70,16 +92,14 @@ class RecorderService: Service() {
this.onStateChange = onStateChange this.onStateChange = onStateChange
} }
// Yield all recordings from 0 to counter fun concatenateFiles(forceConcatenation: Boolean = false): File {
fun getRecordingFilePaths() = sequence<String> { val paths = filePaths.joinToString("|")
for (i in 0 until counter) { val outputFile = "$fileFolder/${originalRecordingStart!!.format(DateTimeFormatter.ISO_DATE_TIME)}.${settings.fileExtension}"
yield("$fileFolder/$i.${getFileExtensions()}")
} if (File(outputFile).exists() && !forceConcatenation) {
return File(outputFile)
} }
fun concatenateAudios(): String {
val paths = getRecordingFilePaths().joinToString("|")
val outputFile = "$fileFolder/concatenated.${getFileExtensions()}"
val command = "-i \"concat:$paths\" -acodec copy $outputFile" val command = "-i \"concat:$paths\" -acodec copy $outputFile"
val session = FFmpegKit.execute(command) val session = FFmpegKit.execute(command)
@ -98,7 +118,7 @@ class RecorderService: Service() {
throw Exception("Failed to concatenate audios") throw Exception("Failed to concatenate audios")
} }
return outputFile return File(outputFile)
} }
private fun startNewRecording() { private fun startNewRecording() {
@ -106,6 +126,8 @@ class RecorderService: Service() {
return return
} }
deleteOldRecordings()
val newRecorder = createRecorder(); val newRecorder = createRecorder();
newRecorder.prepare() newRecorder.prepare()
@ -121,25 +143,62 @@ class RecorderService: Service() {
mediaRecorder = newRecorder mediaRecorder = newRecorder
counter++ counter++
handler.postDelayed(this::startNewRecording, INTERVAL_DURATION)
} }
private fun start(fileFolder: String?, bitRate: Int) { private fun deleteOldRecordings() {
this.fileFolder = fileFolder ?: getRandomFileFolder(this) val timeMultiplier = settings.maxDuration / settings.intervalDuration
this.bitRate = bitRate val earliestCounter = counter - timeMultiplier
File(fileFolder!!).listFiles()?.forEach { file ->
val fileCounter = file.nameWithoutExtension.toIntOrNull() ?: return
if (fileCounter < earliestCounter) {
file.delete()
}
}
}
private fun createRecorder(): MediaRecorder {
filePaths.add(getFilePath())
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
MediaRecorder()
}.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(getFilePath())
setOutputFormat(settings.outputFormat)
setAudioEncoder(settings.encoder)
setAudioEncodingBitRate(settings.bitRate)
setAudioSamplingRate(settings.samplingRate)
setOnErrorListener { mr, what, extra ->
onError?.onError(mr, what, extra)
this@RecorderService.stop()
}
}
}
private fun start() {
filePaths.clear()
// Create folder // Create folder
File(this.fileFolder!!).mkdirs() File(this.fileFolder!!).mkdirs()
println(this.fileFolder) scope.launch {
dataStore.data.collectLatest { preferenceSettings ->
settings = Settings.from(preferenceSettings.audioRecorderSettings)
recordingState = RecorderState.RECORDING recordingState = RecorderState.RECORDING
recordingStart.value = LocalDateTime.now() recordingStart.value = LocalDateTime.now()
originalRecordingStart = recordingStart.value
showNotification() showNotification()
startNewRecording() startNewRecording()
} }
}
}
private fun stop() { private fun stop() {
recordingState = RecorderState.IDLE recordingState = RecorderState.IDLE
@ -192,28 +251,7 @@ class RecorderService: Service() {
).toInt() ).toInt()
} }
private fun createRecorder(): MediaRecorder { private fun getFilePath(): String = "$fileFolder/$counter.${settings.fileExtension}"
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaRecorder(this)
} else {
MediaRecorder()
}.apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFile(getFilePath())
setOutputFormat(getOutputFormat())
setAudioEncoder(getAudioEncoder())
setAudioEncodingBitRate(bitRate!!)
setAudioSamplingRate(getAudioSamplingRate())
setOnErrorListener { mr, what, extra ->
onError?.onError(mr, what, extra)
this@RecorderService.stop()
}
}
}
private fun getFilePath() = "${fileFolder}/${counter}.${getFileExtensions()}"
inner class LocalBinder: Binder() { inner class LocalBinder: Binder() {
fun getService(): RecorderService = this@RecorderService fun getService(): RecorderService = this@RecorderService
@ -231,37 +269,67 @@ class RecorderService: Service() {
} }
companion object { companion object {
fun getOutputFormat(): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
MediaRecorder.OutputFormat.AAC_ADTS
else
MediaRecorder.OutputFormat.THREE_GPP
fun getFileExtensions(): String =
when(getOutputFormat()) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
else -> throw Exception("Unknown output format")
}
fun getAudioSamplingRate(): Int =
when(getOutputFormat()) {
MediaRecorder.OutputFormat.AAC_ADTS -> 96000
MediaRecorder.OutputFormat.THREE_GPP -> 44100
else -> throw Exception("Unknown output format")
}
fun getAudioEncoder(): Int =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
MediaRecorder.AudioEncoder.AAC
else
MediaRecorder.AudioEncoder.AMR_NB
fun getRandomFileFolder(context: Context): String { fun getRandomFileFolder(context: Context): String {
// uuid // uuid
val folder = UUID.randomUUID().toString() val folder = UUID.randomUUID().toString()
return "${context.externalCacheDir!!.absolutePath}/$folder" return "${context.externalCacheDir!!.absolutePath}/$folder"
} }
fun startService(context: Context, connection: ServiceConnection?) {
Intent(context, RecorderService::class.java).also { intent ->
intent.action = RecorderService.Actions.START.toString()
ContextCompat.startForegroundService(context, intent)
if (connection != null) {
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
} }
} }
}
fun stopService(context: Context) {
Intent(context, RecorderService::class.java).also { intent ->
intent.action = RecorderService.Actions.STOP.toString()
context.startService(intent)
}
}
}
}
data class Settings(
val maxDuration: Long,
val intervalDuration: Long,
val bitRate: Int,
val samplingRate: Int,
val outputFormat: Int,
val encoder: Int,
) {
val fileExtension: String
get() = when(outputFormat) {
MediaRecorder.OutputFormat.AAC_ADTS -> "aac"
MediaRecorder.OutputFormat.THREE_GPP -> "3gp"
MediaRecorder.OutputFormat.MPEG_4 -> "mp4"
MediaRecorder.OutputFormat.MPEG_2_TS -> "ts"
MediaRecorder.OutputFormat.WEBM -> "webm"
MediaRecorder.OutputFormat.AMR_NB -> "amr"
MediaRecorder.OutputFormat.AMR_WB -> "awb"
MediaRecorder.OutputFormat.OGG -> "ogg"
else -> "raw"
}
companion object {
fun from(audioRecorderSettings: AudioRecorderSettings): Settings {
return Settings(
intervalDuration = audioRecorderSettings.intervalDuration,
bitRate = audioRecorderSettings.bitRate,
samplingRate = audioRecorderSettings.getSamplingRate(),
outputFormat = audioRecorderSettings.getOutputFormat(),
encoder = audioRecorderSettings.getEncoder(),
maxDuration = audioRecorderSettings.maxDuration,
)
}
}
}

View File

@ -0,0 +1,120 @@
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import app.myzel394.locationtest.services.RecorderService
import app.myzel394.locationtest.ui.components.atoms.Pulsating
import app.myzel394.locationtest.ui.utils.formatDuration
import app.myzel394.locationtest.ui.utils.rememberFileSaverDialog
import java.time.Duration
import java.time.LocalDateTime
@Composable
fun RecordingStatus(
service: RecorderService,
) {
val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/*")
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
// Forces real time update for the text
val transition = rememberInfiniteTransition()
val forceUpdateValue by transition.animateFloat(
initialValue = .999999f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
val distance = Duration.between(service.recordingStart.value, LocalDateTime.now()).toMillis()
Pulsating {
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(Color.Red)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = formatDuration(distance),
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.alpha(forceUpdateValue)
)
}
Spacer(modifier = Modifier.height(8.dp))
LinearProgressIndicator(
progress = service.progress,
modifier = Modifier
.width(300.dp)
.alpha(forceUpdateValue)
)
Spacer(modifier = Modifier.height(32.dp))
Button(
onClick = {
RecorderService.stopService(context)
saveFile(service.concatenateFiles())
},
) {
Icon(
Icons.Default.Save,
contentDescription = null,
)
Text("Save Recording")
}
Button(
onClick = {
RecorderService.stopService(context)
},
colors = ButtonDefaults.textButtonColors(),
) {
Text("Cancel")
}
}
}

View File

@ -0,0 +1,94 @@
package app.myzel394.locationtest.ui.components.AudioRecorder.atoms
import android.content.ServiceConnection
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import app.myzel394.locationtest.services.RecorderService
import app.myzel394.locationtest.ui.utils.rememberFileSaverDialog
import java.time.format.DateTimeFormatter
@Composable
fun StartRecording(
connection: ServiceConnection,
service: RecorderService? = null,
) {
val context = LocalContext.current
val saveFile = rememberFileSaverDialog("audio/*")
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box {}
Button(
onClick = {
RecorderService.startService(context, connection)
},
modifier = Modifier
.semantics {
contentDescription = "Start recording"
}
.size(200.dp)
.clip(shape = CircleShape),
colors = ButtonDefaults.outlinedButtonColors(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
Icons.Default.Mic,
contentDescription = null,
modifier = Modifier
.size(80.dp),
)
Spacer(modifier = Modifier.height(ButtonDefaults.IconSpacing))
Text(
"Start Recording",
style = MaterialTheme.typography.titleSmall,
)
}
}
if (service?.originalRecordingStart != null)
Button(
onClick = {
saveFile(service.concatenateFiles())
}
) {
Icon(
Icons.Default.Save,
contentDescription = null,
modifier = Modifier
.size(ButtonDefaults.IconSize),
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text("Save Recording from ${service.originalRecordingStart!!.format(DateTimeFormatter.ISO_DATE_TIME)}")
}
else
Box {}
}
}

View File

@ -0,0 +1,30 @@
package app.myzel394.locationtest.ui.components.atoms
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@Composable
fun Pulsating(content: @Composable () -> Unit) {
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(1000),
repeatMode = RepeatMode.Reverse
)
)
Box(modifier = Modifier.alpha(alpha)) {
content()
}
}

View File

@ -9,24 +9,41 @@ import android.media.MediaPlayer
import android.os.IBinder import android.os.IBinder
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.navigation.NavController import androidx.navigation.NavController
import app.myzel394.locationtest.services.RecorderService import app.myzel394.locationtest.services.RecorderService
import app.myzel394.locationtest.ui.components.AudioRecorder.atoms.RecordingStatus
import app.myzel394.locationtest.ui.components.AudioRecorder.atoms.StartRecording
import app.myzel394.locationtest.ui.enums.Screen import app.myzel394.locationtest.ui.enums.Screen
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -35,10 +52,6 @@ fun AudioRecorder(
navController: NavController, navController: NavController,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted ->
}
var service by remember { mutableStateOf<RecorderService?>(null) } var service by remember { mutableStateOf<RecorderService?>(null) }
val connection = remember { val connection = remember {
@ -85,51 +98,15 @@ fun AudioRecorder(
) )
}, },
) {padding -> ) {padding ->
Row( Box(
modifier = Modifier.padding(padding), modifier = Modifier
.fillMaxSize()
.padding(padding),
) { ) {
Button( if (isRecording && service != null)
onClick = { RecordingStatus(service = service!!)
// Check audio recording permission else
if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) { StartRecording(connection = connection, service = service)
launcher.launch(Manifest.permission.RECORD_AUDIO)
return@Button
}
if (isRecording) {
Intent(context, RecorderService::class.java).also { intent ->
intent.action = RecorderService.Actions.STOP.toString()
context.startService(intent)
}
} else {
Intent(context, RecorderService::class.java).also { intent ->
intent.action = RecorderService.Actions.START.toString()
ContextCompat.startForegroundService(context, intent)
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
},
) {
Text(text = if (isRecording) "Stop" else "Start")
}
if (!isRecording && service != null)
Button(
onClick = {
val path = service!!.concatenateAudios()
val player = MediaPlayer().apply {
setDataSource(path)
prepare()
}
player.start()
},
) {
Text(text = "Convert")
}
} }
} }
} }

View File

@ -41,6 +41,7 @@ import app.myzel394.locationtest.db.AudioRecorderSettings
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.BitrateTile
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.EncoderTile import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.EncoderTile
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.IntervalDurationTile import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.IntervalDurationTile
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.MaxDurationTile
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.OutputFormatTile import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.OutputFormatTile
import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.SamplingRateTile import app.myzel394.locationtest.ui.components.SettingsScreen.atoms.SamplingRateTile
import app.myzel394.locationtest.ui.components.atoms.GlobalSwitch import app.myzel394.locationtest.ui.components.atoms.GlobalSwitch
@ -107,9 +108,8 @@ fun SettingsScreen(
} }
} }
) )
MaxDurationTile()
IntervalDurationTile() IntervalDurationTile()
BitrateTile()
SamplingRateTile()
AnimatedVisibility(visible = settings.showAdvancedSettings) { AnimatedVisibility(visible = settings.showAdvancedSettings) {
Column { Column {
Divider( Divider(
@ -117,6 +117,8 @@ fun SettingsScreen(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 32.dp) .padding(horizontal = 16.dp, vertical = 32.dp)
) )
BitrateTile()
SamplingRateTile()
OutputFormatTile() OutputFormatTile()
EncoderTile() EncoderTile()
} }

View File

@ -0,0 +1,30 @@
package app.myzel394.locationtest.ui.utils
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat
// From @Bnyro
object PermissionHelper {
fun checkPermissions(context: Context, permissions: Array<String>): Boolean {
permissions.forEach {
if (!hasPermission(context, it)) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(it),
1
)
return false
}
}
return true
}
fun hasPermission(context: Context, permission: String): Boolean {
return ActivityCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}