diff --git a/app/build.gradle b/app/build.gradle index 3751990..3158691 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/locationtest/MainActivity.kt b/app/src/main/java/app/myzel394/locationtest/MainActivity.kt index 340efae..ffb109b 100644 --- a/app/src/main/java/app/myzel394/locationtest/MainActivity.kt +++ b/app/src/main/java/app/myzel394/locationtest/MainActivity.kt @@ -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") - } -} \ No newline at end of file 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 1cbc8a7..0ac90d6 100644 --- a/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/locationtest/db/AppSettings.kt @@ -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() } diff --git a/app/src/main/java/app/myzel394/locationtest/ui/Navigation.kt b/app/src/main/java/app/myzel394/locationtest/ui/Navigation.kt new file mode 100644 index 0000000..bdccb48 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/Navigation.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/app/myzel394/locationtest/ui/enums/Screen.kt b/app/src/main/java/app/myzel394/locationtest/ui/enums/Screen.kt new file mode 100644 index 0000000..29ca6e2 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/enums/Screen.kt @@ -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") + } + } + } +} 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 f992011..6187b90 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 @@ -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") + } + } } } \ 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 new file mode 100644 index 0000000..23d88ea --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/screens/SettingsScreen.kt @@ -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), + ) + } + } + } + } + } +} diff --git a/app/src/main/java/app/myzel394/locationtest/ui/utils/formatters.kt b/app/src/main/java/app/myzel394/locationtest/ui/utils/formatters.kt new file mode 100644 index 0000000..cd17b18 --- /dev/null +++ b/app/src/main/java/app/myzel394/locationtest/ui/utils/formatters.kt @@ -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')}" +}