feat: Add batch duration settings to SettingsScreen

This commit is contained in:
Myzel394 2023-08-02 21:44:18 +02:00
parent 752901e5c4
commit ab1fca418c
No known key found for this signature in database
GPG Key ID: 50098FCA22080F0F
8 changed files with 322 additions and 64 deletions

View File

@ -59,6 +59,7 @@ dependencies {
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'
implementation "androidx.compose.material:material-icons-extended:1.4.3"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@ -79,4 +80,7 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
implementation 'com.maxkeppeler.sheets-compose-dialogs:core:1.2.0'
implementation 'com.maxkeppeler.sheets-compose-dialogs:duration:1.2.0'
}

View File

@ -1,47 +1,27 @@
package app.myzel394.locationtest
import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.myzel394.locationtest.ui.screens.AudioRecorder
import androidx.datastore.dataStore
import app.myzel394.locationtest.db.AppSettingsSerializer
import app.myzel394.locationtest.ui.Navigation
import app.myzel394.locationtest.ui.theme.LocationTestTheme
const val SETTINGS_FILE = "settings.json"
val Context.dataStore by dataStore(
fileName = SETTINGS_FILE,
serializer = AppSettingsSerializer()
)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
LocationTestTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
AudioRecorder()
}
Navigation()
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
LocationTestTheme {
Greeting("Android")
}
}

View File

@ -15,6 +15,10 @@ data class AppSettings(
return copy(showAdvancedSettings = showAdvancedSettings)
}
fun setAudioRecorderSettings(audioRecorderSettings: AudioRecorderSettings): AppSettings {
return copy(audioRecorderSettings = audioRecorderSettings)
}
companion object {
fun getDefaultInstance(): AppSettings = AppSettings()
}

View File

@ -0,0 +1,29 @@
package app.myzel394.locationtest.ui
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import app.myzel394.locationtest.ui.enums.Screen
import app.myzel394.locationtest.ui.screens.AudioRecorder
import app.myzel394.locationtest.ui.screens.SettingsScreen
@Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screen.AudioRecorder.route) {
composable(Screen.AudioRecorder.route) {
AudioRecorder(
navController = navController,
)
}
composable(
route = Screen.Settings.route
) {
SettingsScreen(
navController = navController,
)
}
}
}

View File

@ -0,0 +1,15 @@
package app.myzel394.locationtest.ui.enums
sealed class Screen(val route: String) {
object AudioRecorder : Screen("audio-recorder")
object Settings : Screen("settings")
fun withArgs(vararg args: String): String {
return buildString {
append(route)
args.forEach { arg ->
append("/$arg")
}
}
}
}

View File

@ -10,16 +10,30 @@ import android.os.IBinder
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.Modifier
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import app.myzel394.locationtest.services.RecorderService
import app.myzel394.locationtest.ui.enums.Screen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AudioRecorder() {
fun AudioRecorder(
navController: NavController,
) {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission(),
@ -50,48 +64,72 @@ fun AudioRecorder() {
}
}
Row {
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)
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Audio Recorder")
},
actions = {
IconButton(
onClick = {
navController.navigate(Screen.Settings.route)
},
) {
Icon(
Icons.Default.Settings,
contentDescription = null
)
}
}
},
)
},
) {padding ->
Row(
modifier = Modifier.padding(padding),
) {
Text(text = if (isRecording) "Stop" else "Start")
}
if (!isRecording && service != null)
Button(
onClick = {
val path = service!!.concatenateAudios()
// Check audio recording permission
if (context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED) {
launcher.launch(Manifest.permission.RECORD_AUDIO)
val player = MediaPlayer().apply {
setDataSource(path)
prepare()
return@Button
}
player.start()
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 = "Convert")
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

@ -0,0 +1,167 @@
package app.myzel394.locationtest.ui.screens
import android.app.ProgressDialog.show
import androidx.compose.animation.AnimatedVisibility
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
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.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.datastore.dataStore
import androidx.navigation.NavController
import app.myzel394.locationtest.dataStore
import app.myzel394.locationtest.db.AppSettings
import app.myzel394.locationtest.ui.components.GlobalSwitch
import app.myzel394.locationtest.ui.utils.formatDuration
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
import com.maxkeppeler.sheets.duration.DurationDialog
import com.maxkeppeler.sheets.duration.models.DurationConfig
import com.maxkeppeler.sheets.duration.models.DurationFormat
import com.maxkeppeler.sheets.duration.models.DurationSelection
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.JdkConstants.HorizontalAlignment
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
navController: NavController
) {
Scaffold(
topBar = {
LargeTopAppBar(
title = {
Text(text = "Settings")
},
navigationIcon = {
IconButton(onClick = navController::popBackStack) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) {padding ->
Column(
modifier = Modifier
.fillMaxWidth()
.padding(padding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
val scope = rememberCoroutineScope()
val dataStore = LocalContext.current.dataStore
val settings = dataStore
.data
.collectAsState(initial = AppSettings.getDefaultInstance())
.value
GlobalSwitch(
label = "Advanced Settings",
checked = settings.showAdvancedSettings,
onCheckedChange = {
scope.launch {
dataStore.updateData {
it.setShowAdvancedSettings(it.showAdvancedSettings.not())
}
}
}
)
AnimatedVisibility(visible = settings.showAdvancedSettings) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Icon(
Icons.Default.Mic,
contentDescription = null,
)
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Batch duration",
style = MaterialTheme.typography.labelLarge,
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Record a single batch for this duration. Alibi records multiple batches and deletes the oldest one. When exporting the audio, all batches will be merged together",
style = MaterialTheme.typography.bodySmall,
)
}
Spacer(modifier = Modifier.width(16.dp))
val showDurationPicker = rememberUseCaseState()
DurationDialog(
state = showDurationPicker,
selection = DurationSelection { newTimeInSeconds ->
scope.launch {
dataStore.updateData {
it.setAudioRecorderSettings(
it.audioRecorderSettings.setIntervalDuration(newTimeInSeconds * 1000L)
)
}
}
},
config = DurationConfig(
timeFormat = DurationFormat.HH_MM_SS,
currentTime = settings.audioRecorderSettings.intervalDuration / 1000,
)
)
Button(
onClick = showDurationPicker::show,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
),
shape = MaterialTheme.shapes.medium,
) {
Text(
text = formatDuration(settings.audioRecorderSettings.intervalDuration),
)
}
}
}
}
}
}

View File

@ -0,0 +1,21 @@
package app.myzel394.locationtest.ui.utils
import kotlin.math.floor
fun formatDuration(durationInMilliseconds: Long): String {
if (durationInMilliseconds < 1000) {
return "00:00.$durationInMilliseconds"
}
val totalSeconds = durationInMilliseconds / 1000
if (totalSeconds < 60) {
return "00:${totalSeconds.toString().padStart(2, '0')}"
}
val minutes = floor(totalSeconds / 60.0).toInt()
val seconds = totalSeconds - (minutes * 60)
return "${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}"
}