From 01a6f49b77eb9c9ed0db179046fd72d376d529da Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 12:50:27 +0200 Subject: [PATCH 01/24] feat: Add NotificationSettings --- .../java/app/myzel394/alibi/db/AppSettings.kt | 45 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 ++ 2 files changed, 48 insertions(+) diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index 2fd5abc..f1fda67 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -3,6 +3,7 @@ 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 import kotlinx.serialization.Serializable @@ -14,6 +15,7 @@ import java.time.format.DateTimeFormatter.ISO_DATE_TIME @Serializable data class AppSettings( val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings(), + val notificationSettings: NotificationSettings = NotificationSettings.fromPreset(NotificationSettings.Preset.Default), val hasSeenOnboarding: Boolean = false, val showAdvancedSettings: Boolean = false, val theme: Theme = Theme.SYSTEM, @@ -470,3 +472,46 @@ data class AudioRecorderSettings( } } } + +@Serializable +data class NotificationSettings( + val title: String, + val message: String, + val showOngoing: Boolean, + val preset: Preset? = null, +) { + @Serializable + sealed class Preset( + val titleID: Int, + val messageID: Int, + val showOngoing: Boolean, + ) { + data object Default : Preset( + R.string.ui_audioRecorder_state_recording_title, + R.string.ui_audioRecorder_state_recording_description, + true, + ) + data object Weather : Preset( + R.string.ui_audioRecorder_state_recording_fake_weather_title, + R.string.ui_audioRecorder_state_recording_fake_weather_description, + false, + ) + + data object Player : Preset( + R.string.ui_audioRecorder_state_recording_fake_weather_title, + R.string.ui_audioRecorder_state_recording_fake_weather_description, + false, + ) + } + + companion object { + fun fromPreset(preset: Preset): NotificationSettings { + return NotificationSettings( + title = "", + message = "", + showOngoing = preset.showOngoing, + preset = preset, + ) + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index baa7240..3ca07f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,8 +28,11 @@ Alibi will continue recording in the background and store the last %s minutes at your request Processing Processing Audio, do not close Alibi! You will be automatically prompted to save the file once it\'s ready + Recording Audio Alibi keeps recording in the background + Current Weather + 14° with light chance of rain Welcome to Alibi! Alibi is like a dashcam for your phone. It allows you to record your audio continuously and save the last 30 minutes when you need it. From 6ef60942e04c8d517ec738199a7df2aeaf2e20e3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 16:55:42 +0200 Subject: [PATCH 02/24] fix: Use built in serializer to parse AppSettings --- .../java/app/myzel394/alibi/db/AppSettings.kt | 70 ++----------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index f1fda67..cda7f9a 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -7,6 +7,7 @@ import app.myzel394.alibi.R import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json import org.json.JSONObject import java.io.File import java.time.LocalDateTime @@ -42,45 +43,18 @@ data class AppSettings( DARK, } - fun toJSONObject(): JSONObject { - return JSONObject( - mapOf( - "audioRecorderSettings" to audioRecorderSettings.toJSONObject(), - "hasSeenOnboarding" to hasSeenOnboarding, - "showAdvancedSettings" to showAdvancedSettings, - "theme" to theme.name, - ) - ) - } - fun exportToString(): String { - return JSONObject( - mapOf( - "_meta" to mapOf( - "version" to 1, - "date" to LocalDateTime.now().format(ISO_DATE_TIME), - "app" to "app.myzel394.alibi", - ), - "data" to toJSONObject(), - ) - ).toString(0) + return Json.encodeToString(serializer(), this) } companion object { fun getDefaultInstance(): AppSettings = AppSettings() - fun fromJSONObject(data: JSONObject): AppSettings { - return AppSettings( - audioRecorderSettings = AudioRecorderSettings.fromJSONObject(data.getJSONObject("audioRecorderSettings")), - hasSeenOnboarding = data.getBoolean("hasSeenOnboarding"), - showAdvancedSettings = data.getBoolean("showAdvancedSettings"), - theme = Theme.valueOf(data.getString("theme")), - ) - } - fun fromExportedString(data: String): AppSettings { - val json = JSONObject(data) - return fromJSONObject(json.getJSONObject("data")) + return Json.decodeFromString( + serializer(), + data, + ) } } } @@ -332,20 +306,6 @@ data class AudioRecorderSettings( return supportedFormats.contains(outputFormat) } - fun toJSONObject(): JSONObject { - return JSONObject( - mapOf( - "maxDuration" to maxDuration, - "intervalDuration" to intervalDuration, - "forceExactMaxDuration" to forceExactMaxDuration, - "bitRate" to bitRate, - "samplingRate" to samplingRate, - "outputFormat" to outputFormat, - "encoder" to encoder, - ) - ) - } - companion object { fun getDefaultInstance(): AudioRecorderSettings = AudioRecorderSettings() val EXAMPLE_MAX_DURATIONS = listOf( @@ -452,24 +412,6 @@ data class AudioRecorderSettings( } } }).toMap() - - fun fromJSONObject(data: JSONObject): AudioRecorderSettings { - return AudioRecorderSettings( - maxDuration = data.getLong("maxDuration"), - intervalDuration = data.getLong("intervalDuration"), - forceExactMaxDuration = data.getBoolean("forceExactMaxDuration"), - bitRate = data.getInt("bitRate"), - samplingRate = data.optInt("samplingRate", -1).let { - if (it == -1) null else it - }, - outputFormat = data.optInt("outputFormat", -1).let { - if (it == -1) null else it - }, - encoder = data.optInt("encoder", -1).let { - if (it == -1) null else it - }, - ) - } } } From d0d8996227e16ca98125006cc4e4429dba1ddffa Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:10:24 +0200 Subject: [PATCH 03/24] feat: Add support for custom notification --- .../alibi/db/AppSettingsSerializer.kt | 7 +- .../alibi/services/RecorderService.kt | 70 ++++++++++++++----- .../alibi/ui/models/AudioRecorderModel.kt | 49 +++++++------ 3 files changed, 82 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettingsSerializer.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettingsSerializer.kt index ca91a75..5f8e60e 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettingsSerializer.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettingsSerializer.kt @@ -13,7 +13,7 @@ import java.io.InputStream import java.io.OutputStream import java.time.LocalDateTime -class AppSettingsSerializer: Serializer { +class AppSettingsSerializer : Serializer { override val defaultValue: AppSettings = AppSettings.getDefaultInstance() override suspend fun readFrom(input: InputStream): AppSettings { @@ -39,8 +39,9 @@ class AppSettingsSerializer: Serializer { } } -class LocalDateTimeSerializer: KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) +class LocalDateTimeSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): LocalDateTime { return LocalDateTime.parse(decoder.decodeString()) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 939f933..35c7e14 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -23,7 +23,7 @@ import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit -abstract class RecorderService: Service() { +abstract class RecorderService : Service() { private val binder = RecorderBinder() private var isPaused: Boolean = false @@ -40,6 +40,7 @@ abstract class RecorderService: Service() { private set private lateinit var recordingTimeTimer: ScheduledExecutorService var onRecordingTimeChange: ((Long) -> Unit)? = null + var notificationDetails: NotificationDetails? = null protected abstract fun start() protected abstract fun pause() @@ -61,7 +62,7 @@ abstract class RecorderService: Service() { return super.onStartCommand(intent, flags, startId) } - inner class RecorderBinder: Binder() { + inner class RecorderBinder : Binder() { fun getService(): RecorderService = this@RecorderService } @@ -95,10 +96,12 @@ abstract class RecorderService: Service() { start() } } + RecorderState.PAUSED -> { pause() isPaused = true } + RecorderState.IDLE -> { stop() onDestroy() @@ -109,6 +112,7 @@ abstract class RecorderService: Service() { RecorderState.RECORDING -> { createRecordingTimeTimer() } + RecorderState.PAUSED, RecorderState.IDLE -> { recordingTimeTimer.shutdown() } @@ -121,7 +125,7 @@ abstract class RecorderService: Service() { RecorderState.PAUSED ).contains(newState) && PermissionHelper.hasGranted(this, android.Manifest.permission.POST_NOTIFICATIONS) - ){ + ) { val notification = buildNotification() NotificationManagerCompat.from(this).notify( NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, @@ -148,19 +152,24 @@ abstract class RecorderService: Service() { changeState(RecorderState.IDLE) stopForeground(STOP_FOREGROUND_REMOVE) - NotificationManagerCompat.from(this).cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID) + NotificationManagerCompat.from(this) + .cancel(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID) stopSelf() } - private fun buildStartNotification(): Notification = NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) - .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) - .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) - .setSmallIcon(R.drawable.launcher_foreground) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .build() + private fun buildStartNotification(): Notification = + NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) + .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) + .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) + .setSmallIcon(R.drawable.launcher_foreground) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .build() - private fun getNotificationChangeStateIntent(newState: RecorderState, requestCode: Int): PendingIntent { + private fun getNotificationChangeStateIntent( + newState: RecorderState, + requestCode: Int + ): PendingIntent { return PendingIntent.getService( this, requestCode, @@ -172,14 +181,13 @@ abstract class RecorderService: Service() { ) } - private fun buildNotification(): Notification = when(state) { - RecorderState.RECORDING -> NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) - .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) - .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) - .setSmallIcon(R.drawable.launcher_foreground) + private fun buildNotification(): Notification = when (state) { + RecorderState.RECORDING -> NotificationCompat.Builder( + this, + NotificationHelper.RECORDER_CHANNEL_ID + ) .setPriority(NotificationCompat.PRIORITY_LOW) .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setOngoing(true) .setWhen( Date.from( Calendar @@ -210,8 +218,24 @@ abstract class RecorderService: Service() { getString(R.string.ui_audioRecorder_action_pause_label), getNotificationChangeStateIntent(RecorderState.PAUSED, 2), ) + .apply { + setContentTitle( + notificationDetails?.title + ?: getString(R.string.ui_audioRecorder_state_recording_title) + ) + setContentText( + notificationDetails?.description + ?: getString(R.string.ui_audioRecorder_state_recording_description) + ) + setSmallIcon(notificationDetails?.icon ?: R.drawable.launcher_foreground) + setOngoing(notificationDetails?.isOngoing ?: true) + } .build() - RecorderState.PAUSED -> NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) + + RecorderState.PAUSED -> NotificationCompat.Builder( + this, + NotificationHelper.RECORDER_CHANNEL_ID + ) .setContentTitle(getString(R.string.ui_audioRecorder_state_paused_title)) .setContentText(getString(R.string.ui_audioRecorder_state_paused_description)) .setSmallIcon(R.drawable.launcher_foreground) @@ -236,6 +260,14 @@ abstract class RecorderService: Service() { getNotificationChangeStateIntent(RecorderState.RECORDING, 3), ) .build() + else -> throw IllegalStateException("Invalid state passed to `buildNotification()`") } + + data class NotificationDetails( + val title: String, + val description: String, + val icon: Int, + val isOngoing: Boolean, + ) } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index e9638cf..3b666cb 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt @@ -18,7 +18,7 @@ import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.services.AudioRecorderService import app.myzel394.alibi.services.RecorderService -class AudioRecorderModel: ViewModel() { +class AudioRecorderModel : ViewModel() { var recorderState by mutableStateOf(RecorderState.IDLE) private set var recordingTime by mutableStateOf(null) @@ -48,28 +48,29 @@ class AudioRecorderModel: ViewModel() { private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { - recorderService = ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also {recorder -> - recorder.onStateChange = { state -> - recorderState = state - } - recorder.onRecordingTimeChange = { time -> - recordingTime = time - } - recorder.onAmplitudeChange = { amps -> - amplitudes = amps - onAmplitudeChange() - } - recorder.onError = { - recorderService!!.createLastRecording() - onError() - } - }.also { - it.startRecording() + recorderService = + ((service as RecorderService.RecorderBinder).getService() as AudioRecorderService).also { recorder -> + recorder.onStateChange = { state -> + recorderState = state + } + recorder.onRecordingTimeChange = { time -> + recordingTime = time + } + recorder.onAmplitudeChange = { amps -> + amplitudes = amps + onAmplitudeChange() + } + recorder.onError = { + recorderService!!.createLastRecording() + onError() + } + }.also { + it.startRecording() - recorderState = it.state - recordingTime = it.recordingTime - amplitudes = it.amplitudes - } + recorderState = it.state + recordingTime = it.recordingTime + amplitudes = it.amplitudes + } } override fun onServiceDisconnected(arg0: ComponentName) { @@ -117,6 +118,10 @@ class AudioRecorderModel: ViewModel() { recorderService!!.changeState(RecorderState.RECORDING) } + fun setNotificationDetails(details: RecorderService.NotificationDetails) { + recorderService?.notificationDetails = details + } + fun setMaxAmplitudesAmount(amount: Int) { recorderService?.amplitudesAmount = amount } From ac4102d5cd0a8cf4d3cfae2975913f447ccc2250 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:22:49 +0200 Subject: [PATCH 04/24] feat: Add CustomNotificationTile.kt to SettingsScreen --- .../java/app/myzel394/alibi/db/AppSettings.kt | 3 +- .../atoms/CustomNotificationTile.kt | 53 +++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 2 + app/src/main/res/values/strings.xml | 3 ++ 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index cda7f9a..be44487 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter.ISO_DATE_TIME @Serializable data class AppSettings( val audioRecorderSettings: AudioRecorderSettings = AudioRecorderSettings(), - val notificationSettings: NotificationSettings = NotificationSettings.fromPreset(NotificationSettings.Preset.Default), + val notificationSettings: NotificationSettings? = null, val hasSeenOnboarding: Boolean = false, val showAdvancedSettings: Boolean = false, val theme: Theme = Theme.SYSTEM, @@ -433,6 +433,7 @@ data class NotificationSettings( R.string.ui_audioRecorder_state_recording_description, true, ) + data object Weather : Preset( R.string.ui_audioRecorder_state_recording_fake_weather_title, R.string.ui_audioRecorder_state_recording_fake_weather_description, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt new file mode 100644 index 0000000..288d88c --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt @@ -0,0 +1,53 @@ +package app.myzel394.alibi.ui.components.SettingsScreen.atoms + +import androidx.compose.foundation.clickable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.components.atoms.SettingsTile + +@Composable +fun CustomNotificationTile() { + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + val label = if (settings.notificationSettings == null) + stringResource(R.string.ui_settings_option_customNotification_description_setup) + else stringResource( + R.string.ui_settings_option_customNotification_description_edit + ) + + SettingsTile( + firstModifier = Modifier + .clickable { } + .semantics { contentDescription = label }, + title = stringResource(R.string.ui_settings_option_customNotification_title), + description = label, + leading = { + Icon( + Icons.Default.Notifications, + contentDescription = null, + ) + }, + trailing = { + Icon( + Icons.Default.ChevronRight, + contentDescription = null, + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index 420c117..5235937 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -40,6 +40,7 @@ import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport @@ -145,6 +146,7 @@ fun SettingsScreen( IntervalDurationTile() ForceExactMaxDurationTile() InAppLanguagePicker() + CustomNotificationTile() AnimatedVisibility(visible = settings.showAdvancedSettings) { Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ca07f3..abe5e8a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -71,4 +71,7 @@ Are you sure you want to import these settings? Your current settings will be overwritten! Import settings Settings have been imported successfully! + Custom Notifications + Setup custom recording notifications now + Edit recording notifications \ No newline at end of file From 1e5806000ab16ad4fb617577106f9e4400cc8ccf Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:39:38 +0200 Subject: [PATCH 05/24] feat: Add basic CustomRecordingNotificationsScreen --- .../java/app/myzel394/alibi/ui/Navigation.kt | 20 ++++ .../atoms/CustomNotificationTile.kt | 10 +- .../app/myzel394/alibi/ui/enums/Screen.kt | 7 +- .../CustomRecordingNotificationsScreen.kt | 100 ++++++++++++++++++ .../alibi/ui/screens/SettingsScreen.kt | 2 +- 5 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index 76a1f7c..e9c0f99 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -5,6 +5,8 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.background import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -25,6 +27,7 @@ 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.AudioRecorder +import app.myzel394.alibi.ui.screens.CustomRecordingNotificationsScreen import app.myzel394.alibi.ui.screens.SettingsScreen import app.myzel394.alibi.ui.screens.WelcomeScreen @@ -90,5 +93,22 @@ fun Navigation( audioRecorder = audioRecorder, ) } + composable( + Screen.CustomRecordingNotifications.route, + enterTransition = { + slideInHorizontally( + initialOffsetX = { it -> it / 2 } + ) + fadeIn() + }, + exitTransition = { + slideOutHorizontally( + targetOffsetX = { it -> it / 2 } + ) + fadeOut() + } + ) { + CustomRecordingNotificationsScreen( + navController = navController, + ) + } } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt index 288d88c..76be693 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/SettingsScreen/atoms/CustomNotificationTile.kt @@ -12,13 +12,17 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.navigation.NavController import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.components.atoms.SettingsTile +import app.myzel394.alibi.ui.enums.Screen @Composable -fun CustomNotificationTile() { +fun CustomNotificationTile( + navController: NavController, +) { val dataStore = LocalContext.current.dataStore val settings = dataStore .data @@ -33,7 +37,9 @@ fun CustomNotificationTile() { SettingsTile( firstModifier = Modifier - .clickable { } + .clickable { + navController.navigate(Screen.CustomRecordingNotifications.route) + } .semantics { contentDescription = label }, title = stringResource(R.string.ui_settings_option_customNotification_title), description = label, diff --git a/app/src/main/java/app/myzel394/alibi/ui/enums/Screen.kt b/app/src/main/java/app/myzel394/alibi/ui/enums/Screen.kt index 136d7b8..218bde3 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/enums/Screen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/enums/Screen.kt @@ -1,9 +1,10 @@ package app.myzel394.alibi.ui.enums sealed class Screen(val route: String) { - object AudioRecorder : Screen("audio-recorder") - object Settings : Screen("settings") - object Welcome : Screen("welcome") + data object AudioRecorder : Screen("audio-recorder") + data object Settings : Screen("settings") + data object Welcome : Screen("welcome") + data object CustomRecordingNotifications : Screen("custom-recording-notifications") fun withArgs(vararg args: String): String { return buildString { diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt new file mode 100644 index 0000000..26396c8 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -0,0 +1,100 @@ +package app.myzel394.alibi.ui.screens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Divider +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.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings +import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.MaxDurationTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.OutputFormatTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SamplingRateTile +import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector +import app.myzel394.alibi.ui.components.atoms.GlobalSwitch +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomRecordingNotificationsScreen( + navController: NavController, +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + rememberTopAppBarState() + ) + + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value + + Scaffold( + topBar = { + LargeTopAppBar( + title = { + Text(stringResource(R.string.ui_settings_option_customNotification_title)) + }, + navigationIcon = { + IconButton(onClick = navController::popBackStack) { + Icon( + Icons.Default.ArrowBack, + contentDescription = "Back" + ) + } + }, + scrollBehavior = scrollBehavior, + ) + }, + modifier = Modifier + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt index 5235937..b64a964 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/SettingsScreen.kt @@ -146,7 +146,7 @@ fun SettingsScreen( IntervalDurationTile() ForceExactMaxDurationTile() InAppLanguagePicker() - CustomNotificationTile() + CustomNotificationTile(navController = navController) AnimatedVisibility(visible = settings.showAdvancedSettings) { Column( horizontalAlignment = Alignment.CenterHorizontally, From 17a52fdcb5d2c89bc6842c9c1eca4229d31077ee Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:17:42 +0200 Subject: [PATCH 06/24] feat: Add LandingElement for CustomRecordingNotificationsScreen --- .../atoms/LandingElement.kt | 111 ++++++++++++++++++ .../CustomRecordingNotificationsScreen.kt | 16 +-- .../alibi/ui/utils/PermissionHelper.kt | 15 +++ ...ic_custom_recording_notifications_blob.xml | 13 ++ app/src/main/res/values/strings.xml | 4 + 5 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt create mode 100644 app/src/main/res/drawable/ic_custom_recording_notifications_blob.xml diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt new file mode 100644 index 0000000..08cebaa --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt @@ -0,0 +1,111 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms + +import androidx.compose.foundation.Image +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowRightAlt +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Notifications +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.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.utils.openNotificationsSettings + +@Composable +fun LandingElement( + modifier: Modifier = Modifier, +) { + val context = LocalContext.current + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp) + .then(modifier), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Box() {} + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = R.drawable.ic_custom_recording_notifications_blob), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.tertiaryContainer), + contentDescription = null, + modifier = Modifier + .width(512.dp) + ) + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + tint = MaterialTheme.colorScheme.tertiary, + modifier = Modifier + .size(128.dp) + ) + } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + stringResource(R.string.ui_settings_customNotifications_landing_title), + style = MaterialTheme.typography.headlineMedium, + ) + Text( + stringResource(R.string.ui_settings_customNotifications_landing_description), + style = MaterialTheme.typography.bodySmall, + ) + Button( + onClick = {}, + colors = ButtonDefaults.filledTonalButtonColors(), + ) { + Icon( + Icons.Default.Edit, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text( + stringResource( + R.string.ui_settings_customNotifications_landing_getStarted + ) + ) + } + } + } + Button( + onClick = context::openNotificationsSettings, + colors = ButtonDefaults.textButtonColors(), + ) { + Text( + stringResource(R.string.ui_settings_customNotifications_landing_help_hideNotifications), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index 26396c8..908b4db 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Snackbar import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable @@ -37,6 +38,7 @@ import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile @@ -70,7 +72,7 @@ fun CustomRecordingNotificationsScreen( Scaffold( topBar = { - LargeTopAppBar( + TopAppBar( title = { Text(stringResource(R.string.ui_settings_option_customNotification_title)) }, @@ -88,13 +90,11 @@ fun CustomRecordingNotificationsScreen( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { + if (settings.notificationSettings == null) { + LandingElement( + modifier = Modifier + .padding(padding), + ) } } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/utils/PermissionHelper.kt b/app/src/main/java/app/myzel394/alibi/ui/utils/PermissionHelper.kt index 8d42b6b..f6038e9 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/utils/PermissionHelper.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/utils/PermissionHelper.kt @@ -5,6 +5,7 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri +import android.os.Build import android.provider.Settings import androidx.core.app.ActivityCompat @@ -32,3 +33,17 @@ fun Context.openAppSystemSettings() { data = Uri.fromParts("package", packageName, null) }) } + +fun Context.openNotificationsSettings() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startActivity(Intent().apply { + action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + }) + } else { + startActivity(Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts("package", packageName, null) + }) + } +} diff --git a/app/src/main/res/drawable/ic_custom_recording_notifications_blob.xml b/app/src/main/res/drawable/ic_custom_recording_notifications_blob.xml new file mode 100644 index 0000000..6bb10ef --- /dev/null +++ b/app/src/main/res/drawable/ic_custom_recording_notifications_blob.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abe5e8a..8397559 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,4 +74,8 @@ Custom Notifications Setup custom recording notifications now Edit recording notifications + Don\'t expose yourself + Due to Android\'s restrictions, Alibi has to show a notification while recording. To hide the fact that you\'re using Alibi, you can customize the notification. + Alternatively, you can also simply disable notifications + Create own notification \ No newline at end of file From 119782fb8f2a2d5e6bab480347ede8e24d6ba8d6 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 20:51:50 +0200 Subject: [PATCH 07/24] feat: Adding EditNotificationInput --- .../atoms/EditNotificationInput.kt | 181 ++++++++++++++++++ .../myzel394/alibi/ui/effects/force-update.kt | 23 +++ .../CustomRecordingNotificationsScreen.kt | 19 +- 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt new file mode 100644 index 0000000..1ae7f19 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt @@ -0,0 +1,181 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +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.height +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.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.ui.effects.rememberForceUpdate +import com.maxkeppeler.sheets.input.models.InputText +import java.time.Duration +import java.time.LocalDateTime +import java.time.Period + +@Composable +fun EditNotificationInput( + modifier: Modifier = Modifier, + showOngoing: Boolean, + title: String, + description: String, + onShowOngoingChange: (Boolean) -> Unit, + onTitleChange: (String) -> Unit, + onDescriptionChange: (String) -> Unit, +) { + var ongoingStartTime by remember { mutableStateOf(LocalDateTime.now()) } + + val secondaryColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + + Row( + modifier = Modifier + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f)) + .padding(16.dp) + .then(modifier), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + val headlineSize = 22.dp + + Box( + modifier = Modifier + .size(headlineSize) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondary) + .padding(1.dp), + ) { + Image( + painter = painterResource(id = R.drawable.launcher_foreground), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), + ) + } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.height(headlineSize), + ) { + Text( + stringResource(R.string.app_name), + style = MaterialTheme.typography.bodySmall, + color = secondaryColor, + ) + if (showOngoing) { + Icon( + Icons.Default.Circle, + contentDescription = null, + tint = secondaryColor, + modifier = Modifier + .size(8.dp) + ) + + val fakeAlpha = rememberForceUpdate() + val formattedTime = { + val difference = + Duration.between( + ongoingStartTime, + LocalDateTime.now(), + ) + val minutes = difference.toMinutes() + val seconds = difference.minusMinutes(minutes).seconds + + "${if (minutes < 10) "0$minutes" else minutes}:${if (seconds < 10) "0$seconds" else seconds}" + } + Text( + formattedTime(), + modifier = Modifier.alpha(fakeAlpha), + style = MaterialTheme.typography.bodySmall, + color = secondaryColor, + ) + } + } + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + BasicTextField( + value = title, + onValueChange = onTitleChange, + textStyle = MaterialTheme.typography.titleMedium.copy( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next, + ), + ) + BasicTextField( + value = description, + onValueChange = onDescriptionChange, + textStyle = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done, + ), + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Text( + stringResource(R.string.ui_audioRecorder_action_delete_label), + color = MaterialTheme.colorScheme.secondary, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontWeight = FontWeight.Bold, + ) + Text( + stringResource(R.string.ui_audioRecorder_action_pause_label), + color = MaterialTheme.colorScheme.secondary, + fontSize = MaterialTheme.typography.bodyMedium.fontSize, + fontWeight = FontWeight.Bold, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt b/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt new file mode 100644 index 0000000..be81a62 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/effects/force-update.kt @@ -0,0 +1,23 @@ +package app.myzel394.alibi.ui.effects + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import kotlinx.coroutines.delay + +@Composable +fun rememberForceUpdate( + time: Long = 100L, +): Float { + var tickTack by rememberSaveable { mutableStateOf(1f) } + + LaunchedEffect(tickTack) { + delay(time) + tickTack = if (tickTack == 1f) 0.99f else 1f + } + + return tickTack +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index 908b4db..3dd8bcd 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -38,6 +38,7 @@ import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.EditNotificationInput import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile @@ -91,10 +92,24 @@ fun CustomRecordingNotificationsScreen( .nestedScroll(scrollBehavior.nestedScrollConnection) ) { padding -> if (settings.notificationSettings == null) { - LandingElement( + } + Box( + modifier = Modifier + .padding(padding) + .padding(vertical = 64.dp) + ) { + EditNotificationInput( modifier = Modifier - .padding(padding), + .fillMaxWidth() + .padding(horizontal = 16.dp), + showOngoing = true, + title = "Alibi", + description = "test", + onShowOngoingChange = {}, + onTitleChange = {}, + onDescriptionChange = {}, ) + } } } \ No newline at end of file From dd57ce513e1adb99a6d4223aff25fdb003591b90 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:29:07 +0200 Subject: [PATCH 08/24] feat: Add NotificationPresetSelect; Add more presets --- .../java/app/myzel394/alibi/db/AppSettings.kt | 31 +++++++++- .../atoms/NotificationPresetSelect.kt | 61 +++++++++++++++++++ .../atoms/PreviewIcon.kt | 42 +++++++++++++ .../EditNotificationInput.kt | 21 +++---- .../CustomRecordingNotificationsScreen.kt | 54 ++++++++-------- app/src/main/res/drawable/ic_cloud.xml | 5 ++ app/src/main/res/drawable/ic_download.xml | 5 ++ app/src/main/res/drawable/ic_note.xml | 5 ++ app/src/main/res/drawable/ic_vpn.xml | 5 ++ .../launcher_monochrome_noopacity.xml | 10 +++ app/src/main/res/values/strings.xml | 6 ++ 11 files changed, 198 insertions(+), 47 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt rename app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/{atoms => molecules}/EditNotificationInput.kt (93%) create mode 100644 app/src/main/res/drawable/ic_cloud.xml create mode 100644 app/src/main/res/drawable/ic_download.xml create mode 100644 app/src/main/res/drawable/ic_note.xml create mode 100644 app/src/main/res/drawable/ic_vpn.xml create mode 100644 app/src/main/res/drawable/launcher_monochrome_noopacity.xml diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index be44487..a91b4ab 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -8,7 +8,6 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.ReturnCode import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import org.json.JSONObject import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter.ISO_DATE_TIME @@ -427,23 +426,41 @@ data class NotificationSettings( val titleID: Int, val messageID: Int, val showOngoing: Boolean, + val iconID: Int, ) { data object Default : Preset( R.string.ui_audioRecorder_state_recording_title, R.string.ui_audioRecorder_state_recording_description, true, + R.drawable.launcher_monochrome_noopacity, ) data object Weather : Preset( R.string.ui_audioRecorder_state_recording_fake_weather_title, R.string.ui_audioRecorder_state_recording_fake_weather_description, false, + R.drawable.ic_cloud ) data object Player : Preset( - R.string.ui_audioRecorder_state_recording_fake_weather_title, - R.string.ui_audioRecorder_state_recording_fake_weather_description, + R.string.ui_audioRecorder_state_recording_fake_player_title, + R.string.ui_audioRecorder_state_recording_fake_player_description, + true, + R.drawable.ic_note, + ) + + data object Browser : Preset( + R.string.ui_audioRecorder_state_recording_fake_browser_title, + R.string.ui_audioRecorder_state_recording_fake_browser_description, + true, + R.drawable.ic_download, + ) + + data object VPN : Preset( + R.string.ui_audioRecorder_state_recording_fake_vpn_title, + R.string.ui_audioRecorder_state_recording_fake_vpn_description, false, + R.drawable.ic_vpn, ) } @@ -456,5 +473,13 @@ data class NotificationSettings( preset = preset, ) } + + val PRESETS = listOf( + Preset.Default, + Preset.Weather, + Preset.Player, + Preset.Browser, + Preset.VPN, + ) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt new file mode 100644 index 0000000..f90bbe2 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt @@ -0,0 +1,61 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.db.NotificationSettings + +@Composable +fun NotificationPresetSelect( + modifier: Modifier = Modifier, + preset: NotificationSettings.Preset +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) + .border( + width = 1.dp, + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + .then(modifier), + ) { + PreviewIcon( + modifier = Modifier.size(32.dp), + painter = painterResource(id = preset.iconID), + ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResource(preset.titleID), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + ) + Text( + text = stringResource(preset.messageID), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Normal, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt new file mode 100644 index 0000000..89b02dc --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt @@ -0,0 +1,42 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms + +import androidx.compose.foundation.Image +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +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.graphics.ColorFilter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R + +@Composable +fun PreviewIcon( + modifier: Modifier = Modifier, + painter: Painter, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .then(modifier) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondary) + .padding(1.dp) + ) { + Image( + painter = painter, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt similarity index 93% rename from app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt rename to app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt index 1ae7f19..73e2960 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/EditNotificationInput.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt @@ -1,4 +1,4 @@ -package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween @@ -41,6 +41,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import app.myzel394.alibi.R +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.PreviewIcon import app.myzel394.alibi.ui.effects.rememberForceUpdate import com.maxkeppeler.sheets.input.models.InputText import java.time.Duration @@ -72,19 +73,11 @@ fun EditNotificationInput( ) { val headlineSize = 22.dp - Box( - modifier = Modifier - .size(headlineSize) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.secondary) - .padding(1.dp), - ) { - Image( - painter = painterResource(id = R.drawable.launcher_foreground), - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary), - ) - } + PreviewIcon( + modifier = Modifier.size(headlineSize), + painter = painterResource(id = R.drawable.launcher_foreground) + ) + Column( horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(16.dp), diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index 3dd8bcd..a2cc7a2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -1,33 +1,25 @@ package app.myzel394.alibi.ui.screens -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material3.Divider 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.Snackbar -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -37,24 +29,9 @@ import androidx.navigation.NavController import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings -import app.myzel394.alibi.ui.SUPPORTS_DARK_MODE_NATIVELY -import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.EditNotificationInput -import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.BitrateTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.CustomNotificationTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.EncoderTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ForceExactMaxDurationTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ImportExport -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.InAppLanguagePicker -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.IntervalDurationTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.MaxDurationTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.OutputFormatTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.SamplingRateTile -import app.myzel394.alibi.ui.components.SettingsScreen.atoms.ThemeSelector -import app.myzel394.alibi.ui.components.atoms.GlobalSwitch -import app.myzel394.alibi.ui.components.atoms.MessageBox -import app.myzel394.alibi.ui.components.atoms.MessageType -import kotlinx.coroutines.launch +import app.myzel394.alibi.db.NotificationSettings +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -93,10 +70,12 @@ fun CustomRecordingNotificationsScreen( ) { padding -> if (settings.notificationSettings == null) { } - Box( + Column( modifier = Modifier + .fillMaxSize() .padding(padding) - .padding(vertical = 64.dp) + .padding(vertical = 64.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.SpaceBetween, ) { EditNotificationInput( modifier = Modifier @@ -109,7 +88,22 @@ fun CustomRecordingNotificationsScreen( onTitleChange = {}, onDescriptionChange = {}, ) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(NotificationSettings.PRESETS.size) { + val preset = NotificationSettings.PRESETS[it] + NotificationPresetSelect( + modifier = Modifier + .fillMaxWidth() + .clickable { + + }, + preset = preset, + ) + } + } } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml new file mode 100644 index 0000000..a860632 --- /dev/null +++ b/app/src/main/res/drawable/ic_cloud.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000..987f215 --- /dev/null +++ b/app/src/main/res/drawable/ic_download.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_note.xml b/app/src/main/res/drawable/ic_note.xml new file mode 100644 index 0000000..4bd8b20 --- /dev/null +++ b/app/src/main/res/drawable/ic_note.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_vpn.xml b/app/src/main/res/drawable/ic_vpn.xml new file mode 100644 index 0000000..1339fb3 --- /dev/null +++ b/app/src/main/res/drawable/ic_vpn.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/launcher_monochrome_noopacity.xml b/app/src/main/res/drawable/launcher_monochrome_noopacity.xml new file mode 100644 index 0000000..6bded29 --- /dev/null +++ b/app/src/main/res/drawable/launcher_monochrome_noopacity.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8397559..2dbf736 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -78,4 +78,10 @@ Due to Android\'s restrictions, Alibi has to show a notification while recording. To hide the fact that you\'re using Alibi, you can customize the notification. Alternatively, you can also simply disable notifications Create own notification + Playing Audio + Now playing: Despacito + Downloading attachments.zip + Downloading file... + Connected to VPN + Connection Secured \ No newline at end of file From 5a55619e55f47c192ea61d06809b384d28be63d4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:46:41 +0200 Subject: [PATCH 09/24] feat: Make presets workable --- .../atoms/NotificationPresetSelect.kt | 4 +- .../atoms/PreviewIcon.kt | 2 +- .../molecules/EditNotificationInput.kt | 5 +- .../organisms/NotificationEditor.kt | 110 ++++++++++++++++++ .../CustomRecordingNotificationsScreen.kt | 43 ++----- app/src/main/res/values/strings.xml | 1 + 6 files changed, 127 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt index f90bbe2..3df2775 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/NotificationPresetSelect.kt @@ -29,14 +29,14 @@ fun NotificationPresetSelect( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .clip(MaterialTheme.shapes.large) + .then(modifier) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)) .border( width = 1.dp, shape = MaterialTheme.shapes.large, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f) ) - .padding(horizontal = 16.dp, vertical = 8.dp) - .then(modifier), + .padding(horizontal = 16.dp, vertical = 8.dp), ) { PreviewIcon( modifier = Modifier.size(32.dp), diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt index 89b02dc..1e3c557 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/PreviewIcon.kt @@ -31,7 +31,7 @@ fun PreviewIcon( .then(modifier) .clip(CircleShape) .background(MaterialTheme.colorScheme.secondary) - .padding(1.dp) + .padding(2.dp) ) { Image( painter = painter, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt index 73e2960..1aab0de 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -54,9 +55,11 @@ fun EditNotificationInput( showOngoing: Boolean, title: String, description: String, + icon: Painter, onShowOngoingChange: (Boolean) -> Unit, onTitleChange: (String) -> Unit, onDescriptionChange: (String) -> Unit, + onIconChange: (Int) -> Unit, ) { var ongoingStartTime by remember { mutableStateOf(LocalDateTime.now()) } @@ -75,7 +78,7 @@ fun EditNotificationInput( PreviewIcon( modifier = Modifier.size(headlineSize), - painter = painterResource(id = R.drawable.launcher_foreground) + painter = icon, ) Column( diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt new file mode 100644 index 0000000..685430c --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -0,0 +1,110 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.db.NotificationSettings +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NotificationEditor( + modifier: Modifier = Modifier, + scrollState: ScrollState, +) { + val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title) + val defaultDescription = stringResource(R.string.ui_audioRecorder_state_recording_description) + + var title: String by rememberSaveable { + mutableStateOf(defaultTitle) + } + var description: String by rememberSaveable { + mutableStateOf(defaultDescription) + } + var showOngoing: Boolean by rememberSaveable { + mutableStateOf(true) + } + var icon: Int by rememberSaveable { + mutableStateOf(R.drawable.launcher_monochrome_noopacity) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .then(modifier), + verticalArrangement = Arrangement.SpaceBetween, + ) { + EditNotificationInput( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + showOngoing = true, + title = title, + description = description, + icon = painterResource(icon), + onShowOngoingChange = { + showOngoing = it + }, + onTitleChange = { + title = it + }, + onDescriptionChange = { + description = it + }, + onIconChange = { + icon = it + }, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + for (preset in NotificationSettings.PRESETS) { + val label = stringResource( + R.string.ui_settings_customNotifications_preset_apply_label, + stringResource(preset.titleID) + ) + val presetTitle = stringResource(preset.titleID) + val presetDescription = stringResource(preset.messageID) + + NotificationPresetSelect( + modifier = Modifier + .fillMaxWidth() + .semantics { + contentDescription = label + } + .clickable { + title = presetTitle + description = presetDescription + icon = preset.iconID + showOngoing = preset.showOngoing + }, + preset = preset, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index a2cc7a2..7add6e2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -1,13 +1,15 @@ package app.myzel394.alibi.ui.screens import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,6 +34,7 @@ import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.NotificationSettings import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms.NotificationEditor @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -41,6 +44,7 @@ fun CustomRecordingNotificationsScreen( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState() ) + val scrollState = rememberScrollState() val dataStore = LocalContext.current.dataStore val settings = dataStore @@ -70,40 +74,11 @@ fun CustomRecordingNotificationsScreen( ) { padding -> if (settings.notificationSettings == null) { } - Column( + NotificationEditor( modifier = Modifier - .fillMaxSize() .padding(padding) - .padding(vertical = 64.dp, horizontal = 16.dp), - verticalArrangement = Arrangement.SpaceBetween, - ) { - EditNotificationInput( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - showOngoing = true, - title = "Alibi", - description = "test", - onShowOngoingChange = {}, - onTitleChange = {}, - onDescriptionChange = {}, - ) - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(NotificationSettings.PRESETS.size) { - val preset = NotificationSettings.PRESETS[it] - - NotificationPresetSelect( - modifier = Modifier - .fillMaxWidth() - .clickable { - - }, - preset = preset, - ) - } - } - } + .verticalScroll(scrollState), + scrollState = scrollState, + ) } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2dbf736..8b75438 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,4 +84,5 @@ Downloading file... Connected to VPN Connection Secured + Apply Preset \"%s\" \ No newline at end of file From 54ad067cdf4052ac0fc8c2399a1571caa7f9d3b7 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 23 Oct 2023 21:58:40 +0200 Subject: [PATCH 10/24] feat: Add showOngoing button --- .../molecules/EditNotificationInput.kt | 7 +++ .../organisms/NotificationEditor.kt | 47 +++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt index 1aab0de..c3dcc54 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/EditNotificationInput.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,6 +66,12 @@ fun EditNotificationInput( val secondaryColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + LaunchedEffect(showOngoing) { + if (showOngoing) { + ongoingStartTime = LocalDateTime.now() + } + } + Row( modifier = Modifier .clip(MaterialTheme.shapes.medium) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index 685430c..ee1e921 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -1,27 +1,33 @@ package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxColors +import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.db.NotificationSettings @@ -50,6 +56,7 @@ fun NotificationEditor( mutableStateOf(R.drawable.launcher_monochrome_noopacity) } + // TODO: Add Preview functionality Column( modifier = Modifier .fillMaxSize() @@ -61,7 +68,7 @@ fun NotificationEditor( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - showOngoing = true, + showOngoing = showOngoing, title = title, description = description, icon = painterResource(icon), @@ -79,6 +86,36 @@ fun NotificationEditor( }, ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxSize() + .clip(MaterialTheme.shapes.medium) + .clickable { + showOngoing = showOngoing.not() + } + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(8.dp), + ) { + Checkbox( + checked = showOngoing, + onCheckedChange = { + showOngoing = it + }, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.tertiary, + checkmarkColor = MaterialTheme.colorScheme.onTertiary, + ) + ) + Text( + text = stringResource(R.string.ui_settings_customNotifications_showOngoing_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + fontWeight = FontWeight.Bold, + ) + } + Column( verticalArrangement = Arrangement.spacedBy(4.dp), ) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b75438..3e5c185 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -85,4 +85,5 @@ Connected to VPN Connection Secured Apply Preset \"%s\" + Show Duration \ No newline at end of file From d0885ba877fef0b76d58b0f8e1ea5a9e8730da72 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:10:40 +0200 Subject: [PATCH 11/24] feat: Improve CustomRecordingNotificationsScreen layout --- .../molecules/NotificationPresetsRoulette.kt | 72 ++++++++ .../organisms/NotificationEditor.kt | 173 +++++++++++------- .../CustomRecordingNotificationsScreen.kt | 5 +- app/src/main/res/values/strings.xml | 1 + 4 files changed, 182 insertions(+), 69 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt new file mode 100644 index 0000000..81bd4bc --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt @@ -0,0 +1,72 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import app.myzel394.alibi.R +import app.myzel394.alibi.db.NotificationSettings +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun NotificationPresetsRoulette( + onClick: (String, String, Int, Boolean) -> Unit, +) { + val state = rememberLazyListState() + + LazyRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + state = state, + flingBehavior = rememberSnapFlingBehavior(lazyListState = state) + ) { + items(NotificationSettings.PRESETS.size) { + val preset = NotificationSettings.PRESETS[it] + + val label = stringResource( + R.string.ui_settings_customNotifications_preset_apply_label, + stringResource(preset.titleID) + ) + val presetTitle = stringResource(preset.titleID) + val presetDescription = stringResource(preset.messageID) + + Box( + modifier = Modifier.width( + LocalConfiguration.current.screenWidthDp.dp, + ) + ) { + NotificationPresetSelect( + modifier = Modifier + .fillMaxWidth(.95f) + .align(Alignment.Center) + .semantics { + contentDescription = label + } + .clickable { + onClick( + presetTitle, + presetDescription, + preset.iconID, + preset.showOngoing, + ) + }, + preset = preset, + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index ee1e921..3396077 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -1,28 +1,49 @@ package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior 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.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxColors import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable 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.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -31,10 +52,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.myzel394.alibi.R import app.myzel394.alibi.db.NotificationSettings +import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.NotificationPresetsRoulette -@OptIn(ExperimentalMaterial3Api::class) +val HORIZONTAL_PADDING = 16.dp; + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun NotificationEditor( modifier: Modifier = Modifier, @@ -53,93 +78,105 @@ fun NotificationEditor( mutableStateOf(true) } var icon: Int by rememberSaveable { - mutableStateOf(R.drawable.launcher_monochrome_noopacity) + mutableIntStateOf(R.drawable.launcher_monochrome_noopacity) } // TODO: Add Preview functionality Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) .then(modifier), verticalArrangement = Arrangement.SpaceBetween, ) { - EditNotificationInput( + Column( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - showOngoing = showOngoing, - title = title, - description = description, - icon = painterResource(icon), - onShowOngoingChange = { - showOngoing = it - }, - onTitleChange = { - title = it - }, - onDescriptionChange = { - description = it - }, - onIconChange = { - icon = it - }, - ) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - .clip(MaterialTheme.shapes.medium) - .clickable { - showOngoing = showOngoing.not() - } - .background(MaterialTheme.colorScheme.tertiaryContainer) - .padding(8.dp), + .padding(horizontal = HORIZONTAL_PADDING), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Checkbox( - checked = showOngoing, - onCheckedChange = { + EditNotificationInput( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + showOngoing = showOngoing, + title = title, + description = description, + icon = painterResource(icon), + onShowOngoingChange = { showOngoing = it }, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.tertiary, - checkmarkColor = MaterialTheme.colorScheme.onTertiary, + onTitleChange = { + title = it + }, + onDescriptionChange = { + description = it + }, + onIconChange = { + icon = it + }, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable { + showOngoing = showOngoing.not() + } + .background(MaterialTheme.colorScheme.tertiaryContainer) + .padding(8.dp), + ) { + Checkbox( + checked = showOngoing, + onCheckedChange = { + showOngoing = it + }, + colors = CheckboxDefaults.colors( + checkedColor = MaterialTheme.colorScheme.tertiary, + checkmarkColor = MaterialTheme.colorScheme.onTertiary, + ) ) - ) - Text( - text = stringResource(R.string.ui_settings_customNotifications_showOngoing_label), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onTertiaryContainer, - fontWeight = FontWeight.Bold, - ) + Text( + text = stringResource(R.string.ui_settings_customNotifications_showOngoing_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onTertiaryContainer, + fontWeight = FontWeight.Bold, + ) + } } Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(32.dp), ) { - for (preset in NotificationSettings.PRESETS) { - val label = stringResource( - R.string.ui_settings_customNotifications_preset_apply_label, - stringResource(preset.titleID) - ) - val presetTitle = stringResource(preset.titleID) - val presetDescription = stringResource(preset.messageID) + NotificationPresetsRoulette( + onClick = { presetTitle, presetDescription, presetIcon, presetShowOngoing -> + title = presetTitle + description = presetDescription + icon = presetIcon + showOngoing = presetShowOngoing + } + ) - NotificationPresetSelect( + Button( + onClick = { /*TODO*/ }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = HORIZONTAL_PADDING) + .height(48.dp), + ) { + Icon( + Icons.Default.CheckCircle, + contentDescription = null, modifier = Modifier - .fillMaxWidth() - .semantics { - contentDescription = label - } - .clickable { - title = presetTitle - description = presetDescription - icon = preset.iconID - showOngoing = preset.showOngoing - }, - preset = preset, + .size(ButtonDefaults.IconSize) + ) + Spacer( + modifier = Modifier + .width(ButtonDefaults.IconSpacing) + ) + Text( + stringResource(R.string.ui_settings_customNotifications_save_label) ) } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index 7add6e2..a574751 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -26,6 +27,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavController import app.myzel394.alibi.R @@ -77,7 +80,7 @@ fun CustomRecordingNotificationsScreen( NotificationEditor( modifier = Modifier .padding(padding) - .verticalScroll(scrollState), + .padding(vertical = 16.dp), scrollState = scrollState, ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e5c185..40b54d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -86,4 +86,5 @@ Connection Secured Apply Preset \"%s\" Show Duration + Update notification \ No newline at end of file From 15bb9b405125242b8dc1a195a1ce8e080ead07a0 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:22:05 +0200 Subject: [PATCH 12/24] feat: Improve CustomRecordingNotificationsScreen behavior; Add save support (wip) --- .../java/app/myzel394/alibi/db/AppSettings.kt | 11 ++++ .../atoms/LandingElement.kt | 3 +- .../molecules/NotificationPresetsRoulette.kt | 3 +- .../organisms/NotificationEditor.kt | 21 +++++-- .../CustomRecordingNotificationsScreen.kt | 58 ++++++++++++++++--- 5 files changed, 82 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt index a91b4ab..236d2e6 100644 --- a/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt +++ b/app/src/main/java/app/myzel394/alibi/db/AppSettings.kt @@ -28,6 +28,10 @@ data class AppSettings( return copy(audioRecorderSettings = audioRecorderSettings) } + fun setNotificationSettings(notificationSettings: NotificationSettings?): AppSettings { + return copy(notificationSettings = notificationSettings) + } + fun setHasSeenOnboarding(hasSeenOnboarding: Boolean): AppSettings { return copy(hasSeenOnboarding = hasSeenOnboarding) } @@ -418,6 +422,7 @@ data class AudioRecorderSettings( data class NotificationSettings( val title: String, val message: String, + val iconID: Int, val showOngoing: Boolean, val preset: Preset? = null, ) { @@ -428,6 +433,7 @@ data class NotificationSettings( val showOngoing: Boolean, val iconID: Int, ) { + @Serializable data object Default : Preset( R.string.ui_audioRecorder_state_recording_title, R.string.ui_audioRecorder_state_recording_description, @@ -435,6 +441,7 @@ data class NotificationSettings( R.drawable.launcher_monochrome_noopacity, ) + @Serializable data object Weather : Preset( R.string.ui_audioRecorder_state_recording_fake_weather_title, R.string.ui_audioRecorder_state_recording_fake_weather_description, @@ -442,6 +449,7 @@ data class NotificationSettings( R.drawable.ic_cloud ) + @Serializable data object Player : Preset( R.string.ui_audioRecorder_state_recording_fake_player_title, R.string.ui_audioRecorder_state_recording_fake_player_description, @@ -449,6 +457,7 @@ data class NotificationSettings( R.drawable.ic_note, ) + @Serializable data object Browser : Preset( R.string.ui_audioRecorder_state_recording_fake_browser_title, R.string.ui_audioRecorder_state_recording_fake_browser_description, @@ -456,6 +465,7 @@ data class NotificationSettings( R.drawable.ic_download, ) + @Serializable data object VPN : Preset( R.string.ui_audioRecorder_state_recording_fake_vpn_title, R.string.ui_audioRecorder_state_recording_fake_vpn_description, @@ -470,6 +480,7 @@ data class NotificationSettings( title = "", message = "", showOngoing = preset.showOngoing, + iconID = preset.iconID, preset = preset, ) } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt index 08cebaa..ae45bf1 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt @@ -36,6 +36,7 @@ import app.myzel394.alibi.ui.utils.openNotificationsSettings @Composable fun LandingElement( modifier: Modifier = Modifier, + onOpenEditor: () -> Unit, ) { val context = LocalContext.current @@ -82,7 +83,7 @@ fun LandingElement( style = MaterialTheme.typography.bodySmall, ) Button( - onClick = {}, + onClick = onOpenEditor, colors = ButtonDefaults.filledTonalButtonColors(), ) { Icon( diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt index 81bd4bc..9f019fd 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt @@ -24,7 +24,7 @@ import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms @OptIn(ExperimentalFoundationApi::class) @Composable fun NotificationPresetsRoulette( - onClick: (String, String, Int, Boolean) -> Unit, + onClick: (String, String, Int, Boolean, NotificationSettings.Preset) -> Unit, ) { val state = rememberLazyListState() @@ -62,6 +62,7 @@ fun NotificationPresetsRoulette( presetDescription, preset.iconID, preset.showOngoing, + preset, ) }, preset = preset, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index 3396077..edf34bc 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -56,6 +57,7 @@ import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.NotificationPresetsRoulette +import kotlinx.coroutines.newFixedThreadPoolContext val HORIZONTAL_PADDING = 16.dp; @@ -63,7 +65,7 @@ val HORIZONTAL_PADDING = 16.dp; @Composable fun NotificationEditor( modifier: Modifier = Modifier, - scrollState: ScrollState, + onNotificationChange: (String, String, Int, Boolean, NotificationSettings.Preset?) -> Unit, ) { val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title) val defaultDescription = stringResource(R.string.ui_audioRecorder_state_recording_description) @@ -80,8 +82,10 @@ fun NotificationEditor( var icon: Int by rememberSaveable { mutableIntStateOf(R.drawable.launcher_monochrome_noopacity) } + var preset: NotificationSettings.Preset? by remember { + mutableStateOf(null) + } - // TODO: Add Preview functionality Column( modifier = Modifier .fillMaxSize() @@ -150,16 +154,25 @@ fun NotificationEditor( verticalArrangement = Arrangement.spacedBy(32.dp), ) { NotificationPresetsRoulette( - onClick = { presetTitle, presetDescription, presetIcon, presetShowOngoing -> + onClick = { presetTitle, presetDescription, presetIcon, presetShowOngoing, newPreset -> title = presetTitle description = presetDescription icon = presetIcon showOngoing = presetShowOngoing + preset = newPreset } ) Button( - onClick = { /*TODO*/ }, + onClick = { + onNotificationChange( + title, + description, + icon, + showOngoing, + preset, + ) + }, modifier = Modifier .fillMaxWidth() .padding(horizontal = HORIZONTAL_PADDING) diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index a574751..c6c9008 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -22,7 +22,13 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext @@ -35,9 +41,11 @@ import app.myzel394.alibi.R import app.myzel394.alibi.dataStore import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.NotificationSettings +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.LandingElement import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms.NotificationEditor +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -47,7 +55,6 @@ fun CustomRecordingNotificationsScreen( val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState() ) - val scrollState = rememberScrollState() val dataStore = LocalContext.current.dataStore val settings = dataStore @@ -55,6 +62,16 @@ fun CustomRecordingNotificationsScreen( .collectAsState(initial = AppSettings.getDefaultInstance()) .value + var showEditor: Boolean by rememberSaveable { + mutableStateOf(false) + } + + LaunchedEffect(settings.notificationSettings) { + if (settings.notificationSettings != null) { + showEditor = true + } + } + Scaffold( topBar = { TopAppBar( @@ -75,13 +92,38 @@ fun CustomRecordingNotificationsScreen( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) ) { padding -> - if (settings.notificationSettings == null) { + if (showEditor) { + val scope = rememberCoroutineScope() + + NotificationEditor( + modifier = Modifier + .padding(padding) + .padding(vertical = 16.dp), + onNotificationChange = { title, description, icon, showOngoing, preset -> + scope.launch { + dataStore.updateData { settings -> + settings.setNotificationSettings( + if (preset == null) { + NotificationSettings( + title = title, + message = description, + iconID = icon, + showOngoing = showOngoing, + ) + } else { + NotificationSettings.fromPreset(preset) + } + ) + } + } + } + ) + } else { + LandingElement( + onOpenEditor = { + showEditor = true + } + ) } - NotificationEditor( - modifier = Modifier - .padding(padding) - .padding(vertical = 16.dp), - scrollState = scrollState, - ) } } \ No newline at end of file From f1296c32ec3ad411fa44a5e9c4451a24aad5b24b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:48:15 +0200 Subject: [PATCH 13/24] refactor: Migrating to NotificationViewModel --- .../models/NotificationViewModel.kt | 67 ++++++++++++++ .../molecules/NotificationPresetsRoulette.kt | 4 +- .../organisms/NotificationEditor.kt | 90 ++++++------------- 3 files changed, 94 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt new file mode 100644 index 0000000..93629f7 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt @@ -0,0 +1,67 @@ +package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import app.myzel394.alibi.R +import app.myzel394.alibi.db.NotificationSettings + +class NotificationViewModel : ViewModel() { + // We want to show the actual translated strings of the preset + // in the preview but don't want to save them to the database + // because they should be retrieved in the notification itself. + // Thus we save whether the preset has been changed by the user + private var _presetChanged = false + + private var _title = mutableStateOf("") + val title: String + get() = _title.value + private var _description = mutableStateOf("") + val description: String + get() = _description.value + + var showOngoing: Boolean by mutableStateOf(true) + var icon: Int by mutableIntStateOf(R.drawable.launcher_monochrome_noopacity) + + // `preset` can't be used as a variable name here because + // the compiler throws a strange error then + var notificationPreset: NotificationSettings.Preset? by mutableStateOf(null) + + private var _hasBeenInitialized = false; + + + fun setPreset(title: String, description: String, preset: NotificationSettings.Preset) { + _presetChanged = false + + _title.value = title + _description.value = description + showOngoing = preset.showOngoing + icon = preset.iconID + this.notificationPreset = preset + } + + fun setTitle(title: String) { + _presetChanged = true + _title.value = title + } + + fun setDescription(description: String) { + _presetChanged = true + _description.value = description + } + + fun initialize( + title: String, + description: String, + ) { + if (_hasBeenInitialized) { + return + } + + _title.value = title + _description.value = description + _hasBeenInitialized = true + } +} diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt index 9f019fd..0852cf3 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/molecules/NotificationPresetsRoulette.kt @@ -24,7 +24,7 @@ import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms @OptIn(ExperimentalFoundationApi::class) @Composable fun NotificationPresetsRoulette( - onClick: (String, String, Int, Boolean, NotificationSettings.Preset) -> Unit, + onClick: (String, String, NotificationSettings.Preset) -> Unit, ) { val state = rememberLazyListState() @@ -60,8 +60,6 @@ fun NotificationPresetsRoulette( onClick( presetTitle, presetDescription, - preset.iconID, - preset.showOngoing, preset, ) }, diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index edf34bc..62a04bf 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -1,13 +1,9 @@ package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior 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 @@ -17,47 +13,31 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.Save import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox -import androidx.compose.material3.CheckboxColors import androidx.compose.material3.CheckboxDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import app.myzel394.alibi.R import app.myzel394.alibi.db.NotificationSettings -import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE -import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.atoms.NotificationPresetSelect +import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models.NotificationViewModel import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.NotificationPresetsRoulette -import kotlinx.coroutines.newFixedThreadPoolContext val HORIZONTAL_PADDING = 16.dp; @@ -65,25 +45,17 @@ val HORIZONTAL_PADDING = 16.dp; @Composable fun NotificationEditor( modifier: Modifier = Modifier, + notificationModel: NotificationViewModel = viewModel(), onNotificationChange: (String, String, Int, Boolean, NotificationSettings.Preset?) -> Unit, ) { val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title) val defaultDescription = stringResource(R.string.ui_audioRecorder_state_recording_description) - var title: String by rememberSaveable { - mutableStateOf(defaultTitle) - } - var description: String by rememberSaveable { - mutableStateOf(defaultDescription) - } - var showOngoing: Boolean by rememberSaveable { - mutableStateOf(true) - } - var icon: Int by rememberSaveable { - mutableIntStateOf(R.drawable.launcher_monochrome_noopacity) - } - var preset: NotificationSettings.Preset? by remember { - mutableStateOf(null) + LaunchedEffect(defaultTitle, defaultDescription) { + notificationModel.initialize( + defaultTitle, + defaultDescription, + ) } Column( @@ -101,21 +73,17 @@ fun NotificationEditor( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - showOngoing = showOngoing, - title = title, - description = description, - icon = painterResource(icon), + showOngoing = notificationModel.showOngoing, + title = notificationModel.title, + description = notificationModel.description, + icon = painterResource(notificationModel.icon), onShowOngoingChange = { - showOngoing = it - }, - onTitleChange = { - title = it - }, - onDescriptionChange = { - description = it + notificationModel.showOngoing = it }, + onTitleChange = notificationModel::setTitle, + onDescriptionChange = notificationModel::setDescription, onIconChange = { - icon = it + notificationModel.icon = it }, ) @@ -126,15 +94,15 @@ fun NotificationEditor( .fillMaxWidth() .clip(MaterialTheme.shapes.medium) .clickable { - showOngoing = showOngoing.not() + notificationModel.showOngoing = notificationModel.showOngoing.not() } .background(MaterialTheme.colorScheme.tertiaryContainer) .padding(8.dp), ) { Checkbox( - checked = showOngoing, + checked = notificationModel.showOngoing, onCheckedChange = { - showOngoing = it + notificationModel.showOngoing = it }, colors = CheckboxDefaults.colors( checkedColor = MaterialTheme.colorScheme.tertiary, @@ -154,23 +122,17 @@ fun NotificationEditor( verticalArrangement = Arrangement.spacedBy(32.dp), ) { NotificationPresetsRoulette( - onClick = { presetTitle, presetDescription, presetIcon, presetShowOngoing, newPreset -> - title = presetTitle - description = presetDescription - icon = presetIcon - showOngoing = presetShowOngoing - preset = newPreset - } + onClick = notificationModel::setPreset, ) Button( onClick = { onNotificationChange( - title, - description, - icon, - showOngoing, - preset, + notificationModel.title, + notificationModel.description, + notificationModel.icon, + notificationModel.showOngoing, + notificationModel.notificationPreset, ) }, modifier = Modifier From 6ad7ff12e6dfa197b95b3c1c7199de85d76a6be2 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:31:01 +0200 Subject: [PATCH 14/24] feat: Properly update notification settings --- .../models/NotificationViewModel.kt | 21 ++++-- .../organisms/NotificationEditor.kt | 66 +++++++++++++++---- .../CustomRecordingNotificationsScreen.kt | 16 +---- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt index 93629f7..2d51797 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/models/NotificationViewModel.kt @@ -55,13 +55,26 @@ class NotificationViewModel : ViewModel() { fun initialize( title: String, description: String, + showOngoing: Boolean = true, + icon: Int = R.drawable.launcher_monochrome_noopacity, ) { - if (_hasBeenInitialized) { - return - } - _title.value = title _description.value = description + this.showOngoing = showOngoing + this.icon = icon _hasBeenInitialized = true } + + fun asNotificationSettings(): NotificationSettings { + return if (!_presetChanged && notificationPreset != null) { + NotificationSettings.fromPreset(notificationPreset!!) + } else { + NotificationSettings( + title = title, + message = description, + iconID = icon, + showOngoing = showOngoing, + ) + } + } } diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index 62a04bf..43885b2 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -25,15 +25,19 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import app.myzel394.alibi.R +import app.myzel394.alibi.dataStore +import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.NotificationSettings import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models.NotificationViewModel import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput @@ -41,21 +45,59 @@ import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molec val HORIZONTAL_PADDING = 16.dp; -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun NotificationEditor( modifier: Modifier = Modifier, notificationModel: NotificationViewModel = viewModel(), - onNotificationChange: (String, String, Int, Boolean, NotificationSettings.Preset?) -> Unit, + onNotificationChange: (NotificationSettings) -> Unit, ) { - val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title) - val defaultDescription = stringResource(R.string.ui_audioRecorder_state_recording_description) + val dataStore = LocalContext.current.dataStore + val settings = dataStore + .data + .collectAsState(initial = AppSettings.getDefaultInstance()) + .value - LaunchedEffect(defaultTitle, defaultDescription) { - notificationModel.initialize( - defaultTitle, - defaultDescription, - ) + if (settings.notificationSettings != null) { + val title = settings.notificationSettings.let { + if (it.preset != null) + stringResource(it.preset.titleID) + else + it.title + } + val description = settings.notificationSettings.let { + if (it.preset != null) + stringResource(it.preset.messageID) + else + it.message + } + + LaunchedEffect(Unit) { + notificationModel.initialize( + title, + description, + settings.notificationSettings.showOngoing, + settings.notificationSettings.iconID, + ) + + if (settings.notificationSettings.preset != null) { + notificationModel.setPreset( + title, + description, + settings.notificationSettings.preset + ) + } + } + } else { + val defaultTitle = stringResource(R.string.ui_audioRecorder_state_recording_title) + val defaultDescription = + stringResource(R.string.ui_audioRecorder_state_recording_description) + + LaunchedEffect(Unit) { + notificationModel.initialize( + defaultTitle, + defaultDescription, + ) + } } Column( @@ -128,11 +170,7 @@ fun NotificationEditor( Button( onClick = { onNotificationChange( - notificationModel.title, - notificationModel.description, - notificationModel.icon, - notificationModel.showOngoing, - notificationModel.notificationPreset, + notificationModel.asNotificationSettings() ) }, modifier = Modifier diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index c6c9008..d854dde 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -99,23 +99,13 @@ fun CustomRecordingNotificationsScreen( modifier = Modifier .padding(padding) .padding(vertical = 16.dp), - onNotificationChange = { title, description, icon, showOngoing, preset -> + onNotificationChange = { notificationSettings -> scope.launch { dataStore.updateData { settings -> - settings.setNotificationSettings( - if (preset == null) { - NotificationSettings( - title = title, - message = description, - iconID = icon, - showOngoing = showOngoing, - ) - } else { - NotificationSettings.fromPreset(preset) - } - ) + settings.setNotificationSettings(notificationSettings) } } + navController.popBackStack() } ) } else { From f68baaf1bf7b334cf006cc0bb34d1cd7cf58527d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:34:47 +0200 Subject: [PATCH 15/24] feat: Set NotificationSettings to `null` if using default preset --- .../organisms/NotificationEditor.kt | 2 -- .../alibi/ui/screens/CustomRecordingNotificationsScreen.kt | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index 43885b2..4cd077f 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -1,6 +1,5 @@ package app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.organisms -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -19,7 +18,6 @@ import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CheckboxDefaults -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt index d854dde..7956bf3 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/CustomRecordingNotificationsScreen.kt @@ -102,7 +102,12 @@ fun CustomRecordingNotificationsScreen( onNotificationChange = { notificationSettings -> scope.launch { dataStore.updateData { settings -> - settings.setNotificationSettings(notificationSettings) + settings.setNotificationSettings(notificationSettings.let { + if (it.preset == NotificationSettings.Preset.Default) + null + else + it + }) } } navController.popBackStack() From 1efb57d54047a3ed414933431e31c6711dd151bf Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:40:04 +0200 Subject: [PATCH 16/24] feat: Improve animation --- app/src/main/java/app/myzel394/alibi/ui/Navigation.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt index e9c0f99..bd4bca3 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/Navigation.kt @@ -1,5 +1,6 @@ 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 @@ -103,7 +104,7 @@ fun Navigation( exitTransition = { slideOutHorizontally( targetOffsetX = { it -> it / 2 } - ) + fadeOut() + ) + fadeOut(tween(150)) } ) { CustomRecordingNotificationsScreen( From 81520c48ba4899d21ae3a3f066df062712c132eb Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:02:30 +0200 Subject: [PATCH 17/24] refactor: Use RecorderNotificationHelper for notifications --- .../services/RecorderNotificationHelper.kt | 121 +++++++++++++++++ .../alibi/services/RecorderService.kt | 127 +++--------------- 2 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt new file mode 100644 index 0000000..469a8a6 --- /dev/null +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt @@ -0,0 +1,121 @@ +package app.myzel394.alibi.services + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import app.myzel394.alibi.MainActivity +import app.myzel394.alibi.NotificationHelper +import app.myzel394.alibi.R +import app.myzel394.alibi.enums.RecorderState +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.Calendar +import java.util.Date + +data class RecorderNotificationHelper( + val context: Context, + val details: NotificationDetails? = null, +) { + data class NotificationDetails( + val title: String, + val description: String, + val icon: Int, + val isOngoing: Boolean, + ) + + private fun getNotificationChangeStateIntent( + newState: RecorderState, + requestCode: Int + ): PendingIntent { + return PendingIntent.getService( + context, + requestCode, + Intent(context, AudioRecorderService::class.java).apply { + action = "changeState" + putExtra("newState", newState.name) + }, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + + private fun getIconID(): Int = details?.icon ?: R.drawable.launcher_monochrome_noopacity + + private fun createBaseNotification(): NotificationCompat.Builder { + return NotificationCompat.Builder( + context, + NotificationHelper.RECORDER_CHANNEL_ID + ) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setSmallIcon(getIconID()) + .setContentIntent( + PendingIntent.getActivity( + context, + 0, + Intent(context, MainActivity::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + ) + .setSilent(true) + .setOnlyAlertOnce(true) + .setChronometerCountDown(false) + } + + fun buildStartingNotification(): Notification { + return createBaseNotification() + .setContentTitle(context.getString(R.string.ui_audioRecorder_state_recording_title)) + .setContentText(context.getString(R.string.ui_audioRecorder_state_recording_description)) + .build() + } + + fun buildRecordingNotification(recordingTime: Long): Notification { + return createBaseNotification() + .setUsesChronometer(true) + .setOngoing(details?.isOngoing ?: true) + .setShowWhen(true) + .setWhen( + Date.from( + Calendar + .getInstance() + .also { it.add(Calendar.MILLISECOND, -recordingTime.toInt()) } + .toInstant() + ).time, + ) + .addAction( + R.drawable.ic_cancel, + context.getString(R.string.ui_audioRecorder_action_delete_label), + getNotificationChangeStateIntent(RecorderState.IDLE, 1), + ) + .addAction( + R.drawable.ic_pause, + context.getString(R.string.ui_audioRecorder_action_pause_label), + getNotificationChangeStateIntent(RecorderState.PAUSED, 2), + ) + .setContentTitle( + details?.title + ?: context.getString(R.string.ui_audioRecorder_state_recording_title) + ) + .setContentText( + details?.description + ?: context.getString(R.string.ui_audioRecorder_state_recording_description) + ) + .build() + } + + fun buildPausedNotification(start: LocalDateTime): Notification { + return createBaseNotification() + .setContentTitle(context.getString(R.string.ui_audioRecorder_state_paused_title)) + .setContentText(context.getString(R.string.ui_audioRecorder_state_paused_description)) + .setOngoing(false) + .setUsesChronometer(false) + .setWhen(Date.from(start.atZone(ZoneId.systemDefault()).toInstant()).time) + .addAction( + R.drawable.ic_play, + context.getString(R.string.ui_audioRecorder_action_resume_label), + getNotificationChangeStateIntent(RecorderState.RECORDING, 3), + ) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 35c7e14..6adfa17 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -40,7 +40,7 @@ abstract class RecorderService : Service() { private set private lateinit var recordingTimeTimer: ScheduledExecutorService var onRecordingTimeChange: ((Long) -> Unit)? = null - var notificationDetails: NotificationDetails? = null + var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null protected abstract fun start() protected abstract fun pause() @@ -139,7 +139,7 @@ abstract class RecorderService : Service() { fun startRecording() { recordingStart = LocalDateTime.now() - val notification = buildStartNotification() + val notification = getNotificationHelper().buildStartingNotification() startForeground(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, notification) // Start @@ -157,117 +157,26 @@ abstract class RecorderService : Service() { stopSelf() } - private fun buildStartNotification(): Notification = - NotificationCompat.Builder(this, NotificationHelper.RECORDER_CHANNEL_ID) - .setContentTitle(getString(R.string.ui_audioRecorder_state_recording_title)) - .setContentText(getString(R.string.ui_audioRecorder_state_recording_description)) - .setSmallIcon(R.drawable.launcher_foreground) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .build() - - private fun getNotificationChangeStateIntent( - newState: RecorderState, - requestCode: Int - ): PendingIntent { - return PendingIntent.getService( - this, - requestCode, - Intent(this, AudioRecorderService::class.java).apply { - action = "changeState" - putExtra("newState", newState.name) - }, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) + private fun getNotificationHelper(): RecorderNotificationHelper { + return RecorderNotificationHelper(this, notificationDetails) } - private fun buildNotification(): Notification = when (state) { - RecorderState.RECORDING -> NotificationCompat.Builder( - this, - NotificationHelper.RECORDER_CHANNEL_ID - ) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setWhen( - Date.from( - Calendar - .getInstance() - .also { it.add(Calendar.MILLISECOND, -recordingTime.toInt()) } - .toInstant() - ).time, - ) - .setSilent(true) - .setOnlyAlertOnce(true) - .setUsesChronometer(true) - .setChronometerCountDown(false) - .setContentIntent( - PendingIntent.getActivity( - this, - 0, - Intent(this, MainActivity::class.java), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - ) - .addAction( - R.drawable.ic_cancel, - getString(R.string.ui_audioRecorder_action_delete_label), - getNotificationChangeStateIntent(RecorderState.IDLE, 1), - ) - .addAction( - R.drawable.ic_pause, - getString(R.string.ui_audioRecorder_action_pause_label), - getNotificationChangeStateIntent(RecorderState.PAUSED, 2), - ) - .apply { - setContentTitle( - notificationDetails?.title - ?: getString(R.string.ui_audioRecorder_state_recording_title) - ) - setContentText( - notificationDetails?.description - ?: getString(R.string.ui_audioRecorder_state_recording_description) - ) - setSmallIcon(notificationDetails?.icon ?: R.drawable.launcher_foreground) - setOngoing(notificationDetails?.isOngoing ?: true) + + private fun buildNotification(): Notification { + val notificationHelper = getNotificationHelper() + + return when (state) { + RecorderState.RECORDING -> { + notificationHelper.buildRecordingNotification(recordingTime) } - .build() - RecorderState.PAUSED -> NotificationCompat.Builder( - this, - NotificationHelper.RECORDER_CHANNEL_ID - ) - .setContentTitle(getString(R.string.ui_audioRecorder_state_paused_title)) - .setContentText(getString(R.string.ui_audioRecorder_state_paused_description)) - .setSmallIcon(R.drawable.launcher_foreground) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setOngoing(false) - .setOnlyAlertOnce(true) - .setUsesChronometer(false) - .setWhen(Date.from(recordingStart.atZone(ZoneId.systemDefault()).toInstant()).time) - .setShowWhen(true) - .setContentIntent( - PendingIntent.getActivity( - this, - 0, - Intent(this, MainActivity::class.java), - PendingIntent.FLAG_IMMUTABLE, - ) - ) - .addAction( - R.drawable.ic_play, - getString(R.string.ui_audioRecorder_action_resume_label), - getNotificationChangeStateIntent(RecorderState.RECORDING, 3), - ) - .build() + RecorderState.PAUSED -> { + notificationHelper.buildPausedNotification(recordingStart) + } - else -> throw IllegalStateException("Invalid state passed to `buildNotification()`") + else -> { + throw IllegalStateException("Notification can't be built in state $state") + } + } } - - data class NotificationDetails( - val title: String, - val description: String, - val icon: Int, - val isOngoing: Boolean, - ) } \ No newline at end of file From 37a8a9871e2b432db81583b04e8527dd299903ab Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:47:46 +0200 Subject: [PATCH 18/24] feat: Add custom notification support --- .../services/RecorderNotificationHelper.kt | 28 +++++++++- .../alibi/services/RecorderService.kt | 10 ++++ .../AudioRecorder/molecules/StartRecording.kt | 53 ++++++++++++++++--- .../alibi/ui/models/AudioRecorderModel.kt | 23 ++++++-- .../alibi/ui/screens/AudioRecorder.kt | 6 ++- 5 files changed, 105 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt index 469a8a6..d0835f3 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt @@ -8,7 +8,9 @@ import androidx.core.app.NotificationCompat import app.myzel394.alibi.MainActivity import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.R +import app.myzel394.alibi.db.NotificationSettings import app.myzel394.alibi.enums.RecorderState +import kotlinx.serialization.Serializable import java.time.LocalDateTime import java.time.ZoneId import java.util.Calendar @@ -18,12 +20,36 @@ data class RecorderNotificationHelper( val context: Context, val details: NotificationDetails? = null, ) { + @Serializable data class NotificationDetails( val title: String, val description: String, val icon: Int, val isOngoing: Boolean, - ) + ) { + companion object { + fun fromNotificationSettings( + context: Context, + settings: NotificationSettings, + ): NotificationDetails { + return if (settings.preset == null) { + NotificationDetails( + settings.title, + settings.message, + settings.iconID, + settings.showOngoing, + ) + } else { + NotificationDetails( + context.getString(settings.preset.titleID), + context.getString(settings.preset.messageID), + settings.preset.iconID, + settings.preset.showOngoing, + ) + } + } + } + } private fun getNotificationChangeStateIntent( newState: RecorderState, diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 6adfa17..750d3ea 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -14,6 +14,7 @@ import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.R import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.ui.utils.PermissionHelper +import kotlinx.serialization.json.Json import java.time.LocalDateTime import java.time.ZoneId import java.util.Calendar @@ -51,6 +52,15 @@ abstract class RecorderService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { + "init" -> { + notificationDetails = intent.getStringExtra("notificationDetails")?.let { + Json.decodeFromString( + RecorderNotificationHelper.NotificationDetails.serializer(), + it + ) + } + } + "changeState" -> { val newState = intent.getStringExtra("newState")?.let { RecorderState.valueOf(it) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt index a040e48..0c3c046 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/AudioRecorder/molecules/StartRecording.kt @@ -20,8 +20,15 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.rememberStandardBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -31,22 +38,51 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +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.services.RecorderNotificationHelper import app.myzel394.alibi.ui.BIG_PRIMARY_BUTTON_SIZE import app.myzel394.alibi.ui.components.atoms.PermissionRequester import app.myzel394.alibi.ui.models.AudioRecorderModel import app.myzel394.alibi.ui.utils.rememberFileSaverDialog +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.launch import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @Composable fun StartRecording( audioRecorder: AudioRecorderModel, + // Loading this from parent, because if we load it ourselves + // and permissions have already been granted, initial + // settings will be used, instead of the actual settings. + appSettings: AppSettings ) { val context = LocalContext.current + // We can't get the current `notificationDetails` inside the + // `onPermissionAvailable` function. We'll instead use this hack + // with `LaunchedEffect` to get the current value. + var startRecording by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(startRecording) { + if (startRecording) { + startRecording = false + audioRecorder.notificationDetails = appSettings.notificationSettings.let { + if (it == null) + null + else + RecorderNotificationHelper.NotificationDetails.fromNotificationSettings( + context, + it + ) + } + audioRecorder.startRecording(context) + } + } + Column( modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween, @@ -57,14 +93,12 @@ fun StartRecording( permission = Manifest.permission.RECORD_AUDIO, icon = Icons.Default.Mic, onPermissionAvailable = { - audioRecorder.startRecording(context) + startRecording = true }, ) { trigger -> val label = stringResource(R.string.ui_audioRecorder_action_start_label) Button( - onClick = { - trigger() - }, + onClick = trigger, modifier = Modifier .semantics { contentDescription = label @@ -98,7 +132,10 @@ fun StartRecording( .value Text( - stringResource(R.string.ui_audioRecorder_action_start_description, settings.audioRecorderSettings.maxDuration / 1000 / 60), + stringResource( + R.string.ui_audioRecorder_action_start_description, + settings.audioRecorderSettings.maxDuration / 1000 / 60 + ), style = MaterialTheme.typography.bodySmall.copy( color = MaterialTheme.colorScheme.onSurfaceVariant, ), @@ -115,7 +152,8 @@ fun StartRecording( ) { val label = stringResource( R.string.ui_audioRecorder_action_saveOldRecording_label, - DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).format(audioRecorder.lastRecording!!.recordingStart), + DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL) + .format(audioRecorder.lastRecording!!.recordingStart), ) Button( modifier = Modifier @@ -140,8 +178,7 @@ fun StartRecording( Text(label) } } - } - else + } else Spacer(modifier = Modifier.weight(1f)) } } \ No newline at end of file diff --git a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt index 3b666cb..7e278ee 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/models/AudioRecorderModel.kt @@ -13,10 +13,14 @@ 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.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() { var recorderState by mutableStateOf(RecorderState.IDLE) @@ -45,6 +49,7 @@ class AudioRecorderModel : ViewModel() { var onRecordingSave: () -> Unit = {} var onError: () -> Unit = {} + var notificationDetails: RecorderNotificationHelper.NotificationDetails? = null private val connection = object : ServiceConnection { override fun onServiceConnected(className: ComponentName, service: IBinder) { @@ -90,7 +95,19 @@ class AudioRecorderModel : ViewModel() { context.unbindService(connection) } - val intent = Intent(context, AudioRecorderService::class.java) + val intent = Intent(context, AudioRecorderService::class.java).apply { + action = "init" + + if (notificationDetails != null) { + putExtra( + "notificationDetails", + Json.encodeToString( + RecorderNotificationHelper.NotificationDetails.serializer(), + notificationDetails!!, + ), + ) + } + } ContextCompat.startForegroundService(context, intent) context.bindService(intent, connection, Context.BIND_AUTO_CREATE) } @@ -118,10 +135,6 @@ class AudioRecorderModel : ViewModel() { recorderService!!.changeState(RecorderState.RECORDING) } - fun setNotificationDetails(details: RecorderService.NotificationDetails) { - recorderService?.notificationDetails = details - } - fun setMaxAmplitudesAmount(amount: Int) { recorderService?.amplitudesAmount = amount } diff --git a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt index 1e121bf..99845dc 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/screens/AudioRecorder.kt @@ -38,6 +38,7 @@ 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.ui.effects.rememberSettings import app.myzel394.alibi.ui.models.AudioRecorderModel import kotlinx.coroutines.launch @@ -171,10 +172,13 @@ fun AudioRecorder( .fillMaxSize() .padding(padding), ) { + val appSettings = + context.dataStore.data.collectAsState(AppSettings.getDefaultInstance()).value + if (audioRecorder.isInRecording) RecordingStatus(audioRecorder = audioRecorder) else - StartRecording(audioRecorder = audioRecorder) + StartRecording(audioRecorder = audioRecorder, appSettings = appSettings) } } } \ No newline at end of file From 26f17869d67cec030e78df1b45087b862b95a217 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:50:58 +0200 Subject: [PATCH 19/24] feat: Add message info to NotificationEditor --- .../organisms/NotificationEditor.kt | 7 +++++++ .../myzel394/alibi/ui/components/atoms/MessageBox.kt | 10 +++++++--- app/src/main/res/values/strings.xml | 1 + 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt index 4cd077f..7aacc97 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/organisms/NotificationEditor.kt @@ -40,6 +40,8 @@ import app.myzel394.alibi.db.NotificationSettings import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.models.NotificationViewModel import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.EditNotificationInput import app.myzel394.alibi.ui.components.CustomRecordingNotificationsScreen.molecules.NotificationPresetsRoulette +import app.myzel394.alibi.ui.components.atoms.MessageBox +import app.myzel394.alibi.ui.components.atoms.MessageType val HORIZONTAL_PADDING = 16.dp; @@ -109,6 +111,11 @@ fun NotificationEditor( .padding(horizontal = HORIZONTAL_PADDING), verticalArrangement = Arrangement.spacedBy(16.dp), ) { + MessageBox( + type = MessageType.SURFACE, + message = stringResource(R.string.ui_settings_customNotifications_edit_help) + ) + EditNotificationInput( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt index cf0f0c9..1c594ff 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/atoms/MessageBox.kt @@ -29,17 +29,19 @@ fun MessageBox( title: String? = null, ) { val isDark = rememberIsInDarkMode() - val containerColor = when(type) { + val containerColor = when (type) { MessageType.ERROR -> MaterialTheme.colorScheme.errorContainer MessageType.INFO -> MaterialTheme.colorScheme.tertiaryContainer MessageType.SUCCESS -> Color.Green.copy(alpha = 0.3f) MessageType.WARNING -> Color.Yellow.copy(alpha = 0.3f) + MessageType.SURFACE -> MaterialTheme.colorScheme.surfaceVariant } - val onContainerColor = when(type) { + val onContainerColor = when (type) { MessageType.ERROR -> MaterialTheme.colorScheme.onError MessageType.INFO -> MaterialTheme.colorScheme.onTertiaryContainer MessageType.SUCCESS -> Color.Green MessageType.WARNING -> Color.Yellow + MessageType.SURFACE -> MaterialTheme.colorScheme.onSurfaceVariant } val textColor = if (isDark) onContainerColor else MaterialTheme.colorScheme.onSurface val backgroundColor = if (isDark) containerColor else onContainerColor @@ -53,9 +55,10 @@ fun MessageBox( .then(modifier) ) { Icon( - imageVector = when(type) { + imageVector = when (type) { MessageType.ERROR -> Icons.Default.Error MessageType.INFO -> Icons.Default.Info + MessageType.SURFACE -> Icons.Default.Info MessageType.SUCCESS -> Icons.Default.Check MessageType.WARNING -> Icons.Default.Warning }, @@ -84,6 +87,7 @@ fun MessageBox( enum class MessageType { ERROR, INFO, + SURFACE, SUCCESS, WARNING, } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40b54d9..33e9c64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -87,4 +87,5 @@ Apply Preset \"%s\" Show Duration Update notification + This is a preview for your notification. You can edit the title and the message. At the bottom you can find some presets. \ No newline at end of file From 573a70cca841e20e9f58b11dd3924e01f09e55f1 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:02:37 +0200 Subject: [PATCH 20/24] fix: Fix padding --- .../CustomRecordingNotificationsScreen/atoms/LandingElement.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt index ae45bf1..01c5f1e 100644 --- a/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt +++ b/app/src/main/java/app/myzel394/alibi/ui/components/CustomRecordingNotificationsScreen/atoms/LandingElement.kt @@ -43,7 +43,7 @@ fun LandingElement( Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 32.dp) + .padding(horizontal = 32.dp, vertical = 64.dp) .then(modifier), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, From c6d20c74f27393a57aa5b79266965cfcda632a57 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:11:56 +0200 Subject: [PATCH 21/24] fix: Properly set on going for notifications --- .../app/myzel394/alibi/services/RecorderNotificationHelper.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt index d0835f3..86ad9bb 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderNotificationHelper.kt @@ -98,9 +98,9 @@ data class RecorderNotificationHelper( fun buildRecordingNotification(recordingTime: Long): Notification { return createBaseNotification() - .setUsesChronometer(true) + .setUsesChronometer(details?.isOngoing ?: true) .setOngoing(details?.isOngoing ?: true) - .setShowWhen(true) + .setShowWhen(details?.isOngoing ?: true) .setWhen( Date.from( Calendar From f80d3cbe16ec10f19a5b0ed3c8527c85533a2e7b Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:17:51 +0200 Subject: [PATCH 22/24] fix(ci-cd): Upload all APKs --- .github/workflows/build-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-testing.yaml b/.github/workflows/build-testing.yaml index c2793e9..680ea90 100644 --- a/.github/workflows/build-testing.yaml +++ b/.github/workflows/build-testing.yaml @@ -25,4 +25,4 @@ jobs: - name: Upload APK uses: actions/upload-artifact@v3 with: - path: app/build/outputs/apk/debug/app-debug.apk + path: app/build/outputs/apk/debug/app-*-debug.apk From cb43a1b37199c08aa33231ed5f6173a1ff4e62ad Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:43:51 +0200 Subject: [PATCH 23/24] chore: Adapt to new foreground service --- .../app/myzel394/alibi/services/RecorderService.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 750d3ea..1024f87 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -5,10 +5,13 @@ import android.app.Notification import android.app.PendingIntent import android.app.Service import android.content.Intent +import android.content.pm.ServiceInfo import android.os.Binder +import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import androidx.core.app.ServiceCompat import app.myzel394.alibi.MainActivity import app.myzel394.alibi.NotificationHelper import app.myzel394.alibi.R @@ -150,7 +153,16 @@ abstract class RecorderService : Service() { recordingStart = LocalDateTime.now() val notification = getNotificationHelper().buildStartingNotification() - startForeground(NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, notification) + ServiceCompat.startForeground( + this, + NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, + notification, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } else { + 0 + }, + ) // Start changeState(RecorderState.RECORDING) From 16e6fd8fc208e238a7200e096ea279691505bc7a Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:49:15 +0200 Subject: [PATCH 24/24] fix: Improve code --- .../main/java/app/myzel394/alibi/services/RecorderService.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt index 1024f87..f3a24ce 100644 --- a/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt +++ b/app/src/main/java/app/myzel394/alibi/services/RecorderService.kt @@ -152,11 +152,10 @@ abstract class RecorderService : Service() { fun startRecording() { recordingStart = LocalDateTime.now() - val notification = getNotificationHelper().buildStartingNotification() ServiceCompat.startForeground( this, NotificationHelper.RECORDER_CHANNEL_NOTIFICATION_ID, - notification, + getNotificationHelper().buildStartingNotification(), if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE } else {