diff --git a/app/build.gradle b/app/build.gradle index 53e905b..316f638 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,6 +9,7 @@ android { compileSdk 33 defaultConfig { + multiDexEnabled true applicationId "app.myzel394.locationtest" minSdk 24 targetSdk 33 diff --git a/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt b/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt index 16ebaba..7fa9f0e 100644 --- a/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt @@ -106,7 +106,7 @@ data class 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") } diff --git a/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt b/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt index 0e68c7d..6338d1b 100644 --- a/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/locationtest/services/RecorderService.kt @@ -3,6 +3,7 @@ package app.myzel394.locationtest.services import android.app.Service import android.content.Context import android.content.Intent +import android.content.ServiceConnection import android.media.MediaRecorder import android.os.Binder import android.os.Build @@ -12,21 +13,30 @@ import android.os.Looper import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat 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.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.time.LocalDateTime import java.time.ZoneId +import java.time.format.DateTimeFormatter import java.util.Date import java.util.UUID; -const val INTERVAL_DURATION = 10000L - class RecorderService: Service() { private val binder = LocalBinder() private val handler = Handler(Looper.getMainLooper()) + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) private var mediaRecorder: MediaRecorder? = null private var onError: MediaRecorder.OnErrorListener? = null @@ -34,27 +44,30 @@ class RecorderService: Service() { private var counter = 0 + lateinit var settings: Settings + var recordingStart = mutableStateOf(null) private set var fileFolder: String? = null private set - var bitRate: Int? = null - private set var recordingState: RecorderState = RecorderState.IDLE private set - val isRecording: Boolean get() = recordingStart.value != null + val filePaths = mutableListOf() + + var originalRecordingStart: LocalDateTime? = null + private set + override fun onBind(p0: Intent?): IBinder = binder override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { Actions.START.toString() -> { - val fileFolder = intent.getStringExtra("fileFolder") - val bitRate = intent.getIntExtra("bitRate", 320000) + fileFolder = getRandomFileFolder(this) - start(fileFolder, bitRate) + start() } Actions.STOP.toString() -> stop() } @@ -62,6 +75,15 @@ class RecorderService: Service() { 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) { this.onError = onError } @@ -70,16 +92,14 @@ class RecorderService: Service() { this.onStateChange = onStateChange } - // Yield all recordings from 0 to counter - fun getRecordingFilePaths() = sequence { - for (i in 0 until counter) { - yield("$fileFolder/$i.${getFileExtensions()}") - } - } + fun concatenateFiles(forceConcatenation: Boolean = false): File { + val paths = filePaths.joinToString("|") + val outputFile = "$fileFolder/${originalRecordingStart!!.format(DateTimeFormatter.ISO_DATE_TIME)}.${settings.fileExtension}" + + 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 session = FFmpegKit.execute(command) @@ -98,7 +118,7 @@ class RecorderService: Service() { throw Exception("Failed to concatenate audios") } - return outputFile + return File(outputFile) } private fun startNewRecording() { @@ -106,6 +126,8 @@ class RecorderService: Service() { return } + deleteOldRecordings() + val newRecorder = createRecorder(); newRecorder.prepare() @@ -121,24 +143,61 @@ class RecorderService: Service() { mediaRecorder = newRecorder counter++ - handler.postDelayed(this::startNewRecording, INTERVAL_DURATION) } - private fun start(fileFolder: String?, bitRate: Int) { - this.fileFolder = fileFolder ?: getRandomFileFolder(this) - this.bitRate = bitRate + private fun deleteOldRecordings() { + val timeMultiplier = settings.maxDuration / settings.intervalDuration + 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 File(this.fileFolder!!).mkdirs() - println(this.fileFolder) + scope.launch { + dataStore.data.collectLatest { preferenceSettings -> + settings = Settings.from(preferenceSettings.audioRecorderSettings) + recordingState = RecorderState.RECORDING + recordingStart.value = LocalDateTime.now() + originalRecordingStart = recordingStart.value - recordingState = RecorderState.RECORDING - recordingStart.value = LocalDateTime.now() - - showNotification() - - startNewRecording() + showNotification() + startNewRecording() + } + } } private fun stop() { @@ -187,33 +246,12 @@ class RecorderService: Service() { val offset = ZoneId.of("UTC").rules.getOffset(recordingStart.value) return ( - recordingStart.value!!.toEpochSecond(offset) - - LocalDateTime.of(2023, 1, 1, 1, 1).toEpochSecond(offset) - ).toInt() + recordingStart.value!!.toEpochSecond(offset) - + LocalDateTime.of(2023, 1, 1, 1, 1).toEpochSecond(offset) + ).toInt() } - private fun createRecorder(): MediaRecorder { - 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()}" + private fun getFilePath(): String = "$fileFolder/$counter.${settings.fileExtension}" inner class LocalBinder: Binder() { fun getService(): RecorderService = this@RecorderService @@ -231,37 +269,67 @@ class RecorderService: Service() { } 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 { // uuid val folder = UUID.randomUUID().toString() 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) + } + } } -} \ No newline at end of file +} + +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, + ) + } + } +} + diff --git a/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt new file mode 100644 index 0000000..84229a0 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/RecordingStatus.kt @@ -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") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/StartRecording.kt b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/StartRecording.kt new file mode 100644 index 0000000..4dcb558 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/components/AudioRecorder/atoms/StartRecording.kt @@ -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 {} + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/locationtest/ui/components/atoms/Pulsating.kt b/app/src/main/java/app/myzel394/locationtest/ui/components/atoms/Pulsating.kt new file mode 100644 index 0000000..01b987e --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/components/atoms/Pulsating.kt @@ -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() + } +} diff --git a/app/src/main/java/app/myzel394/locationtest/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/locationtest/ui/screens/AudioRecorder.kt index 6187b90..0a839f5 100644 --- a/app/src/main/java/app/myzel394/locationtest/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/locationtest/ui/screens/AudioRecorder.kt @@ -9,24 +9,41 @@ import android.media.MediaPlayer import android.os.IBinder import androidx.activity.compose.rememberLauncherForActivityResult 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.fillMaxSize 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.filled.Mic import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.runtime.* +import androidx.compose.ui.Alignment 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.navigation.NavController 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 @OptIn(ExperimentalMaterial3Api::class) @@ -35,10 +52,6 @@ fun AudioRecorder( navController: NavController, ) { val context = LocalContext.current - val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission(), - ) { isGranted -> - } var service by remember { mutableStateOf(null) } val connection = remember { @@ -85,51 +98,15 @@ fun AudioRecorder( ) }, ) {padding -> - Row( - modifier = Modifier.padding(padding), + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), ) { - Button( - onClick = { - // Check audio recording permission - if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) { - 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") - } + if (isRecording && service != null) + RecordingStatus(service = service!!) + else + StartRecording(connection = connection, service = service) } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/locationtest/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/locationtest/ui/screens/SettingsScreen.kt index ed22a55..f1e2f1c 100644 --- a/app/src/main/java/app/myzel394/locationtest/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/locationtest/ui/screens/SettingsScreen.kt @@ -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.EncoderTile 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.SamplingRateTile import app.myzel394.locationtest.ui.components.atoms.GlobalSwitch @@ -107,9 +108,8 @@ fun SettingsScreen( } } ) + MaxDurationTile() IntervalDurationTile() - BitrateTile() - SamplingRateTile() AnimatedVisibility(visible = settings.showAdvancedSettings) { Column { Divider( @@ -117,6 +117,8 @@ fun SettingsScreen( .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 32.dp) ) + BitrateTile() + SamplingRateTile() OutputFormatTile() EncoderTile() } diff --git a/app/src/main/java/app/myzel394/locationtest/ui/utils/PermissionHelper.kt b/app/src/main/java/app/myzel394/locationtest/ui/utils/PermissionHelper.kt new file mode 100644 index 0000000..01c0689 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/utils/PermissionHelper.kt @@ -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): 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 + } +}