diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 94d84d2..e9ffe26 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -33,8 +33,8 @@ android {
applicationId = "org.nsh07.pomodoro"
minSdk = 26
targetSdk = 36
- versionCode = 6
- versionName = "1.2.0"
+ versionCode = 7
+ versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/schemas/org.nsh07.pomodoro.data.AppDatabase/2.json b/app/schemas/org.nsh07.pomodoro.data.AppDatabase/2.json
new file mode 100644
index 0000000..54e93e3
--- /dev/null
+++ b/app/schemas/org.nsh07.pomodoro.data.AppDatabase/2.json
@@ -0,0 +1,133 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 2,
+ "identityHash": "4691d636a1c16c8cd33dc1bf0602190c",
+ "entities": [
+ {
+ "tableName": "int_preference",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "boolean_preference",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` INTEGER NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "string_preference",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "stat",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`date` TEXT NOT NULL, `focusTimeQ1` INTEGER NOT NULL, `focusTimeQ2` INTEGER NOT NULL, `focusTimeQ3` INTEGER NOT NULL, `focusTimeQ4` INTEGER NOT NULL, `breakTime` INTEGER NOT NULL, PRIMARY KEY(`date`))",
+ "fields": [
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTimeQ1",
+ "columnName": "focusTimeQ1",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTimeQ2",
+ "columnName": "focusTimeQ2",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTimeQ3",
+ "columnName": "focusTimeQ3",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "focusTimeQ4",
+ "columnName": "focusTimeQ4",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "breakTime",
+ "columnName": "breakTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "date"
+ ]
+ }
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4691d636a1c16c8cd33dc1bf0602190c')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a5372a3..19ff772 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,6 +6,7 @@
+
+
+ @Query("SELECT value FROM string_preference WHERE `key` = :key")
+ suspend fun getStringPreference(key: String): String?
+
+ @Query("SELECT value FROM string_preference WHERE `key` = :key")
+ fun getStringPreferenceFlow(key: String): Flow
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt
index 438af3f..4d53239 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/PreferenceRepository.kt
@@ -9,12 +9,13 @@ package org.nsh07.pomodoro.data
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
/**
* Interface for reading/writing app preferences to the app's database. This style of storage aims
- * to mimic the Preferences DataStore library, preventing the requirement of migration if the
- * database schema changes.
+ * to mimic the Preferences DataStore library, preventing the requirement of migration if new
+ * preferences are added
*/
interface PreferenceRepository {
/**
@@ -22,11 +23,41 @@ interface PreferenceRepository {
*/
suspend fun saveIntPreference(key: String, value: Int): Int
+ /**
+ * Saves a boolean preference key-value pair to the database.
+ */
+ suspend fun saveBooleanPreference(key: String, value: Boolean): Boolean
+
+ /**
+ * Saves a string preference key-value pair to the database.
+ */
+ suspend fun saveStringPreference(key: String, value: String): String
+
/**
* Retrieves an integer preference key-value pair from the database.
*/
suspend fun getIntPreference(key: String): Int?
+ /**
+ * Retrieves a boolean preference key-value pair from the database.
+ */
+ suspend fun getBooleanPreference(key: String): Boolean?
+
+ /**
+ * Retrieves a boolean preference key-value pair as a flow from the database.
+ */
+ fun getBooleanPreferenceFlow(key: String): Flow
+
+ /**
+ * Retrieves a string preference key-value pair from the database.
+ */
+ suspend fun getStringPreference(key: String): String?
+
+ /**
+ * Retrieves a string preference key-value pair as a flow from the database.
+ */
+ fun getStringPreferenceFlow(key: String): Flow
+
/**
* Erases all integer preference key-value pairs in the database. Do note that the default values
* will need to be rewritten manually
@@ -47,11 +78,39 @@ class AppPreferenceRepository(
value
}
+ override suspend fun saveBooleanPreference(key: String, value: Boolean): Boolean =
+ withContext(ioDispatcher) {
+ preferenceDao.insertBooleanPreference(BooleanPreference(key, value))
+ value
+ }
+
+ override suspend fun saveStringPreference(key: String, value: String): String =
+ withContext(ioDispatcher) {
+ preferenceDao.insertStringPreference(StringPreference(key, value))
+ value
+ }
+
override suspend fun getIntPreference(key: String): Int? = withContext(ioDispatcher) {
preferenceDao.getIntPreference(key)
}
+ override suspend fun getBooleanPreference(key: String): Boolean? = withContext(ioDispatcher) {
+ preferenceDao.getBooleanPreference(key)
+ }
+
+ override fun getBooleanPreferenceFlow(key: String): Flow =
+ preferenceDao.getBooleanPreferenceFlow(key)
+
+ override suspend fun getStringPreference(key: String): String? = withContext(ioDispatcher) {
+ preferenceDao.getStringPreference(key)
+ }
+
+ override fun getStringPreferenceFlow(key: String): Flow =
+ preferenceDao.getStringPreferenceFlow(key)
+
override suspend fun resetSettings() = withContext(ioDispatcher) {
preferenceDao.resetIntPreferences()
+ preferenceDao.resetBooleanPreferences()
+ preferenceDao.resetStringPreferences()
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
index 0283a17..e1e3068 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt
@@ -7,6 +7,11 @@
package org.nsh07.pomodoro.data
+import android.net.Uri
+import android.provider.Settings
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.lightColorScheme
+
/**
* Interface that holds the timer durations for each timer type. This repository maintains a single
* source of truth for the timer durations for the various ViewModels in the app.
@@ -15,7 +20,17 @@ interface TimerRepository {
var focusTime: Long
var shortBreakTime: Long
var longBreakTime: Long
+
var sessionLength: Int
+
+ var timerFrequency: Float
+
+ var alarmEnabled: Boolean
+ var vibrateEnabled: Boolean
+
+ var colorScheme: ColorScheme
+
+ var alarmSoundUri: Uri?
}
/**
@@ -26,4 +41,10 @@ class AppTimerRepository : TimerRepository {
override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4
+ override var timerFrequency: Float = 10f
+ override var alarmEnabled = true
+ override var vibrateEnabled = true
+ override var colorScheme = lightColorScheme()
+ override var alarmSoundUri: Uri? =
+ Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
index 82c2ff4..a3c348c 100644
--- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
@@ -7,9 +7,9 @@ import android.media.MediaPlayer
import android.os.Build
import android.os.IBinder
import android.os.SystemClock
-import android.provider.Settings
-import androidx.compose.material3.ColorScheme
-import androidx.compose.material3.lightColorScheme
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.os.VibratorManager
import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -17,41 +17,35 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.TomatoApplication
-import org.nsh07.pomodoro.data.AppContainer
-import org.nsh07.pomodoro.data.StatRepository
-import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
-import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
import kotlin.text.Typography.middleDot
class TimerService : Service() {
- private lateinit var appContainer: AppContainer
-
- private lateinit var timerRepository: TimerRepository
- private lateinit var statRepository: StatRepository
- private lateinit var notificationManager: NotificationManagerCompat
- private lateinit var notificationBuilder: NotificationCompat.Builder
- private lateinit var _timerState: MutableStateFlow
- private lateinit var _time: MutableStateFlow
-
- val timeStateFlow by lazy {
- _time.asStateFlow()
+ private val appContainer by lazy {
+ (application as TomatoApplication).container
}
- var time: Long
+ private val timerRepository by lazy { appContainer.appTimerRepository }
+ private val statRepository by lazy { appContainer.appStatRepository }
+ private val notificationManager by lazy { NotificationManagerCompat.from(this) }
+ private val notificationBuilder by lazy { appContainer.notificationBuilder }
+ private val _timerState by lazy { appContainer.timerState }
+ private val _time by lazy { appContainer.time }
+
+ private val timeStateFlow by lazy { _time.asStateFlow() }
+
+ private var time: Long
get() = timeStateFlow.value
set(value) = _time.update { value }
- lateinit var timerState: StateFlow
+ private val timerState by lazy { _timerState.asStateFlow() }
private var cycles = 0
private var startTime = 0L
@@ -62,29 +56,26 @@ class TimerService : Service() {
private val scope = CoroutineScope(Dispatchers.IO + job)
private val skipScope = CoroutineScope(Dispatchers.IO + job)
- private lateinit var alarm: MediaPlayer
+ private var alarm: MediaPlayer? = null
- private var cs: ColorScheme = lightColorScheme()
+ private val vibrator by lazy {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
+ vibratorManager.defaultVibrator
+ } else {
+ @Suppress("DEPRECATION") getSystemService(VIBRATOR_SERVICE) as Vibrator
+ }
+ }
+
+ private val cs by lazy { timerRepository.colorScheme }
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onCreate() {
- appContainer = (application as TomatoApplication).container
- timerRepository = appContainer.appTimerRepository
- statRepository = appContainer.appStatRepository
- notificationManager = NotificationManagerCompat.from(this)
- notificationBuilder = appContainer.notificationBuilder
- _timerState = appContainer.timerState
- _time = appContainer.time
-
- timerState = _timerState.asStateFlow()
-
- alarm = MediaPlayer.create(
- this,
- Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
- )
+ super.onCreate()
+ alarm = MediaPlayer.create(this, timerRepository.alarmSoundUri)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -103,32 +94,26 @@ class TimerService : Service() {
Actions.SKIP.toString() -> skipTimer(true)
Actions.STOP_ALARM.toString() -> stopAlarm()
+
+ Actions.UPDATE_ALARM_TONE.toString() -> updateAlarmTone()
}
return super.onStartCommand(intent, flags, startId)
}
private fun toggleTimer() {
if (timerState.value.timerRunning) {
- notificationBuilder
- .clearActions()
- .addTimerActions(
- this,
- R.drawable.play,
- "Start"
- )
+ notificationBuilder.clearActions().addTimerActions(
+ this, R.drawable.play, "Start"
+ )
showTimerNotification(time.toInt(), paused = true)
_timerState.update { currentState ->
currentState.copy(timerRunning = false)
}
pauseTime = SystemClock.elapsedRealtime()
} else {
- notificationBuilder
- .clearActions()
- .addTimerActions(
- this,
- R.drawable.pause,
- "Stop"
- )
+ notificationBuilder.clearActions().addTimerActions(
+ this, R.drawable.pause, "Stop"
+ )
_timerState.update { it.copy(timerRunning = true) }
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
@@ -147,7 +132,8 @@ class TimerService : Service() {
else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
}
- iterations = (iterations + 1) % 10
+ iterations =
+ (iterations + 1) % timerRepository.timerFrequency.toInt().coerceAtLeast(1)
if (iterations == 0) showTimerNotification(time.toInt())
@@ -165,7 +151,7 @@ class TimerService : Service() {
}
}
- delay(100)
+ delay((1000f / timerRepository.timerFrequency).toLong())
}
}
}
@@ -208,40 +194,42 @@ class TimerService : Service() {
)
.setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})")
.setStyle(
- NotificationCompat.ProgressStyle().also {
- // Add all the Focus, Short break and long break intervals in order
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
- // Android 16 and later supports live updates
- // Set progress bar sections if on Baklava or later
- for (i in 0..= Build.VERSION_CODES.BAKLAVA) {
+ // Android 16 and later supports live updates
+ // Set progress bar sections if on Baklava or later
+ for (i in 0.. timerRepository.focusTime.toInt()
+ TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
+ else -> timerRepository.longBreakTime.toInt()
+ }
+ )
)
}
- } else {
- it.addProgressSegment(
- NotificationCompat.ProgressStyle.Segment(
- when (timerState.value.timerMode) {
- TimerMode.FOCUS -> timerRepository.focusTime.toInt()
- TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
- else -> timerRepository.longBreakTime.toInt()
- }
- )
- )
}
- }
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
(totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt()
@@ -254,7 +242,7 @@ class TimerService : Service() {
)
if (complete) {
- alarm.start()
+ startAlarm()
_timerState.update { currentState ->
currentState.copy(alarmRinging = true)
}
@@ -328,9 +316,30 @@ class TimerService : Service() {
}
}
+ fun startAlarm() {
+ if (timerRepository.alarmEnabled) alarm?.start()
+
+ if (timerRepository.vibrateEnabled) {
+ if (!vibrator.hasVibrator()) {
+ return
+ }
+ val vibrationPattern = longArrayOf(0, 1000, 1000, 1000)
+ val repeat = 2
+ val effect = VibrationEffect.createWaveform(vibrationPattern, repeat)
+ vibrator.vibrate(effect)
+ }
+ }
+
fun stopAlarm() {
- alarm.pause()
- alarm.seekTo(0)
+ if (timerRepository.alarmEnabled) {
+ alarm?.pause()
+ alarm?.seekTo(0)
+ }
+
+ if (timerRepository.vibrateEnabled) {
+ vibrator.cancel()
+ }
+
_timerState.update { currentState ->
currentState.copy(alarmRinging = false)
}
@@ -340,12 +349,15 @@ class TimerService : Service() {
TimerMode.FOCUS -> timerRepository.focusTime.toInt()
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
else -> timerRepository.longBreakTime.toInt()
- },
- paused = true,
- complete = false
+ }, paused = true, complete = false
)
}
+ fun updateAlarmTone() {
+ alarm?.release()
+ alarm = MediaPlayer.create(this, timerRepository.alarmSoundUri)
+ }
+
suspend fun saveTimeToDb() {
when (timerState.value.timerMode) {
TimerMode.FOCUS -> statRepository.addFocusTime(
@@ -374,10 +386,11 @@ class TimerService : Service() {
job.cancel()
saveTimeToDb()
notificationManager.cancel(1)
+ alarm?.release()
}
}
enum class Actions {
- TOGGLE, SKIP, RESET, STOP_ALARM
+ TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
index b2437f6..0a1e0d9 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
@@ -28,14 +28,11 @@ import androidx.compose.material3.ShortNavigationBarItem
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -50,6 +47,7 @@ import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
+import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@@ -69,16 +67,19 @@ fun AppScreen(
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
val layoutDirection = LocalLayoutDirection.current
- val haptic = LocalHapticFeedback.current
val motionScheme = motionScheme
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
- LaunchedEffect(uiState.timerMode) {
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
- }
-
val backStack = rememberNavBackStack(Screen.Timer)
+ if (uiState.alarmRinging)
+ AlarmDialog {
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.STOP_ALARM.toString()
+ context.startService(it)
+ }
+ }
+
Scaffold(
bottomBar = {
val wide = remember {
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
index 5355f9c..6420566 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt
@@ -7,8 +7,17 @@
package org.nsh07.pomodoro.ui.settingsScreen
+import android.app.Activity
+import android.content.Intent
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -18,8 +27,10 @@ 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.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
@@ -32,10 +43,11 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
-import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
+import androidx.compose.material3.Switch
+import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
@@ -50,18 +62,25 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import androidx.core.net.toUri
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import org.nsh07.pomodoro.R
+import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
+import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
+import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
+import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@OptIn(ExperimentalMaterial3Api::class)
@@ -70,6 +89,7 @@ fun SettingsScreenRoot(
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
+ val context = LocalContext.current
val focusTimeInputFieldState = rememberSaveable(saver = TextFieldState.Saver) {
viewModel.focusTimeTextFieldState
}
@@ -80,6 +100,10 @@ fun SettingsScreenRoot(
viewModel.longBreakTimeTextFieldState
}
+ val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
+ val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
+ val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
+
val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver(
viewModel.sessionsSliderState.onValueChangeFinished,
@@ -94,6 +118,18 @@ fun SettingsScreenRoot(
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
+ alarmEnabled = alarmEnabled,
+ vibrateEnabled = vibrateEnabled,
+ alarmSound = alarmSound,
+ onAlarmEnabledChange = viewModel::saveAlarmEnabled,
+ onVibrateEnabledChange = viewModel::saveVibrateEnabled,
+ onAlarmSoundChanged = {
+ viewModel.saveAlarmSound(it)
+ Intent(context, TimerService::class.java).apply {
+ action = TimerService.Actions.RESET.toString()
+ context.startService(this)
+ }
+ },
modifier = modifier
)
}
@@ -105,9 +141,63 @@ private fun SettingsScreen(
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
+ alarmEnabled: Boolean,
+ vibrateEnabled: Boolean,
+ alarmSound: String,
+ onAlarmEnabledChange: (Boolean) -> Unit,
+ onVibrateEnabledChange: (Boolean) -> Unit,
+ onAlarmSoundChanged: (Uri?) -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
+ val switchColors = SwitchDefaults.colors(
+ checkedIconColor = colorScheme.primary,
+ )
+
+ val context = LocalContext.current
+
+ val ringtonePickerLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ val uri =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ result.data?.getParcelableExtra(
+ RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
+ Uri::class.java
+ )
+ } else {
+ @Suppress("DEPRECATION")
+ result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
+ }
+ onAlarmSoundChanged(uri)
+ }
+ }
+
+ val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
+ putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
+ putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, "Alarm sound")
+ putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
+ }
+
+ val switchItems = remember(alarmEnabled, vibrateEnabled) {
+ listOf(
+ SettingsSwitchItem(
+ checked = alarmEnabled,
+ icon = R.drawable.alarm_on,
+ label = "Alarm",
+ description = "Ring alarm when a timer completes",
+ onClick = onAlarmEnabledChange
+ ),
+ SettingsSwitchItem(
+ checked = vibrateEnabled,
+ icon = R.drawable.mobile_vibrate,
+ label = "Vibrate",
+ description = "Vibrate when a timer completes",
+ onClick = onVibrateEnabledChange
+ )
+ )
+ }
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
@@ -155,10 +245,10 @@ private fun SettingsScreen(
MinuteInputField(
state = focusTimeInputFieldState,
shape = RoundedCornerShape(
- topStart = 16.dp,
- bottomStart = 16.dp,
- topEnd = 4.dp,
- bottomEnd = 4.dp
+ topStart = topListItemShape.topStart,
+ bottomStart = topListItemShape.topStart,
+ topEnd = topListItemShape.bottomStart,
+ bottomEnd = topListItemShape.bottomStart
),
imeAction = ImeAction.Next
)
@@ -174,7 +264,7 @@ private fun SettingsScreen(
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
- shape = RoundedCornerShape(4.dp),
+ shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
}
@@ -190,10 +280,10 @@ private fun SettingsScreen(
MinuteInputField(
state = longBreakTimeInputFieldState,
shape = RoundedCornerShape(
- topStart = 4.dp,
- bottomStart = 4.dp,
- topEnd = 16.dp,
- bottomEnd = 16.dp
+ topStart = bottomListItemShape.topStart,
+ bottomStart = bottomListItemShape.topStart,
+ topEnd = bottomListItemShape.bottomStart,
+ bottomEnd = bottomListItemShape.bottomStart
),
imeAction = ImeAction.Done
)
@@ -224,7 +314,68 @@ private fun SettingsScreen(
}
},
colors = listItemColors,
- modifier = Modifier.clip(shapes.large)
+ modifier = Modifier.clip(topListItemShape)
+ )
+ }
+ item {
+ ListItem(
+ leadingContent = {
+ Icon(painterResource(R.drawable.alarm), null)
+ },
+ headlineContent = { Text("Alarm sound") },
+ supportingContent = {
+ Text(
+ remember(alarmSound) {
+ RingtoneManager.getRingtone(context, alarmSound.toUri())
+ .getTitle(context)
+ }
+ )
+ },
+ colors = listItemColors,
+ modifier = Modifier
+ .clip(bottomListItemShape)
+ .clickable(onClick = { ringtonePickerLauncher.launch(intent) })
+ )
+ }
+ item { Spacer(Modifier.height(12.dp)) }
+ itemsIndexed(switchItems) { index, item ->
+ ListItem(
+ leadingContent = {
+ Icon(painterResource(item.icon), contentDescription = null)
+ },
+ headlineContent = { Text(item.label) },
+ supportingContent = { Text(item.description) },
+ trailingContent = {
+ Switch(
+ checked = item.checked,
+ onCheckedChange = { item.onClick(it) },
+ thumbContent = {
+ if (item.checked) {
+ Icon(
+ painter = painterResource(R.drawable.check),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ } else {
+ Icon(
+ painter = painterResource(R.drawable.clear),
+ contentDescription = null,
+ modifier = Modifier.size(SwitchDefaults.IconSize),
+ )
+ }
+ },
+ colors = switchColors
+ )
+ },
+ colors = listItemColors,
+ modifier = Modifier
+ .clip(
+ when (index) {
+ 0 -> topListItemShape
+ switchItems.lastIndex -> bottomListItemShape
+ else -> middleListItemShape
+ }
+ )
)
}
item {
@@ -275,7 +426,21 @@ fun SettingsScreenPreview() {
shortBreakTimeInputFieldState = rememberTextFieldState((5 * 60 * 1000).toString()),
longBreakTimeInputFieldState = rememberTextFieldState((15 * 60 * 1000).toString()),
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
+ alarmEnabled = true,
+ vibrateEnabled = true,
+ alarmSound = "null",
+ onAlarmEnabledChange = {},
+ onVibrateEnabledChange = {},
+ onAlarmSoundChanged = {},
modifier = Modifier.fillMaxSize()
)
}
}
+
+data class SettingsSwitchItem(
+ val checked: Boolean,
+ @DrawableRes val icon: Int,
+ val label: String,
+ val description: String,
+ val onClick: (Boolean) -> Unit
+)
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
index c3454e5..dc2439d 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt
@@ -7,6 +7,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
+import android.net.Uri
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState
@@ -20,6 +21,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.AppPreferenceRepository
@@ -44,6 +46,15 @@ class SettingsViewModel(
onValueChangeFinished = ::updateSessionLength
)
+ val currentAlarmSound = timerRepository.alarmSoundUri.toString()
+
+ val alarmSound =
+ preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
+ val alarmEnabled =
+ preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
+ val vibrateEnabled =
+ preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
+
init {
viewModelScope.launch(Dispatchers.IO) {
snapshotFlow { focusTimeTextFieldState.text }
@@ -92,6 +103,27 @@ class SettingsViewModel(
}
}
+ fun saveAlarmEnabled(enabled: Boolean) {
+ viewModelScope.launch {
+ preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
+ timerRepository.alarmEnabled = enabled
+ }
+ }
+
+ fun saveVibrateEnabled(enabled: Boolean) {
+ viewModelScope.launch {
+ preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
+ timerRepository.vibrateEnabled = enabled
+ }
+ }
+
+ fun saveAlarmSound(uri: Uri?) {
+ viewModelScope.launch {
+ preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
+ }
+ timerRepository.alarmSoundUri = uri
+ }
+
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt
new file mode 100644
index 0000000..b7dea55
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Shape.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.nsh07.pomodoro.ui.theme
+
+import androidx.compose.foundation.shape.CornerBasedShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.MaterialTheme.shapes
+import androidx.compose.runtime.Composable
+
+object TomatoShapeDefaults {
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ val topListItemShape: RoundedCornerShape
+ @Composable get() =
+ RoundedCornerShape(
+ topStart = shapes.largeIncreased.topStart,
+ topEnd = shapes.largeIncreased.topEnd,
+ bottomStart = shapes.extraSmall.bottomStart,
+ bottomEnd = shapes.extraSmall.bottomStart
+ )
+
+ val middleListItemShape: RoundedCornerShape
+ @Composable get() = RoundedCornerShape(shapes.extraSmall.topStart)
+
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ val bottomListItemShape: RoundedCornerShape
+ @Composable get() =
+ RoundedCornerShape(
+ topStart = shapes.extraSmall.topStart,
+ topEnd = shapes.extraSmall.topEnd,
+ bottomStart = shapes.largeIncreased.bottomStart,
+ bottomEnd = shapes.largeIncreased.bottomEnd
+ )
+
+ @OptIn(ExperimentalMaterial3ExpressiveApi::class)
+ val cardShape: CornerBasedShape
+ @Composable get() = shapes.largeIncreased
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt
index 556e88d..6d67506 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt
@@ -69,7 +69,7 @@ fun AlarmDialog(
onClick = stopAlarm,
modifier = Modifier.align(Alignment.End),
) {
- Text("Ok")
+ Text("Dismiss")
}
}
}
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
index ba3a1b2..daed55a 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt
@@ -63,7 +63,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
@@ -89,6 +91,7 @@ fun TimerScreen(
modifier: Modifier = Modifier
) {
val motionScheme = motionScheme
+ val haptic = LocalHapticFeedback.current
val color by animateColorAsState(
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
@@ -111,9 +114,6 @@ fun TimerScreen(
onResult = {}
)
- if (timerState.alarmRinging)
- AlarmDialog { onAction(TimerAction.StopAlarm) }
-
Column(modifier = modifier) {
TopAppBar(
title = {
@@ -301,6 +301,10 @@ fun TimerScreen(
FilledIconToggleButton(
onCheckedChange = { checked ->
onAction(TimerAction.ToggleTimer)
+
+ if (checked) haptic.performHapticFeedback(HapticFeedbackType.ToggleOn)
+ else haptic.performHapticFeedback(HapticFeedbackType.ToggleOff)
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
@@ -358,7 +362,10 @@ fun TimerScreen(
customItem(
{
FilledTonalIconButton(
- onClick = { onAction(TimerAction.ResetTimer) },
+ onClick = {
+ onAction(TimerAction.ResetTimer)
+ haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
+ },
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
@@ -395,7 +402,10 @@ fun TimerScreen(
customItem(
{
FilledTonalIconButton(
- onClick = { onAction(TimerAction.SkipTimer(fromButton = true)) },
+ onClick = {
+ onAction(TimerAction.SkipTimer(fromButton = true))
+ haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
+ },
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer
),
diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
index a173dd4..71cb79f 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt
@@ -8,7 +8,9 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.app.Application
+import android.provider.Settings
import androidx.compose.material3.ColorScheme
+import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -30,6 +32,7 @@ import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate
+import java.time.temporal.ChronoUnit
@OptIn(FlowPreview::class)
class TimerViewModel(
@@ -77,16 +80,33 @@ class TimerViewModel(
timerRepository.sessionLength
)
+ timerRepository.alarmEnabled =
+ preferenceRepository.getBooleanPreference("alarm_enabled")
+ ?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
+ timerRepository.vibrateEnabled =
+ preferenceRepository.getBooleanPreference("vibrate_enabled")
+ ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
+
+ timerRepository.alarmSoundUri = (
+ preferenceRepository.getStringPreference("alarm_sound")
+ ?: preferenceRepository.saveStringPreference(
+ "alarm_sound",
+ (Settings.System.DEFAULT_ALARM_ALERT_URI
+ ?: Settings.System.DEFAULT_RINGTONE_URI).toString()
+ )
+ ).toUri()
+
resetTimer()
var lastDate = statRepository.getLastDate()
val today = LocalDate.now()
// Fills dates between today and lastDate with 0s to ensure continuous history
- while ((lastDate?.until(today)?.days ?: -1) > 0) {
- lastDate = lastDate?.plusDays(1)
- statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
- }
+ if (lastDate != null)
+ while (ChronoUnit.DAYS.between(lastDate, today) > 0) {
+ lastDate = lastDate?.plusDays(1)
+ statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
+ }
delay(1500)
diff --git a/app/src/main/res/drawable/alarm_on.xml b/app/src/main/res/drawable/alarm_on.xml
new file mode 100644
index 0000000..da06d7f
--- /dev/null
+++ b/app/src/main/res/drawable/alarm_on.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/check.xml b/app/src/main/res/drawable/check.xml
new file mode 100644
index 0000000..954c1ed
--- /dev/null
+++ b/app/src/main/res/drawable/check.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/clear.xml b/app/src/main/res/drawable/clear.xml
new file mode 100644
index 0000000..970bfaa
--- /dev/null
+++ b/app/src/main/res/drawable/clear.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/hourglass.xml b/app/src/main/res/drawable/hourglass.xml
deleted file mode 100644
index a9fdbda..0000000
--- a/app/src/main/res/drawable/hourglass.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/hourglass_filled.xml b/app/src/main/res/drawable/hourglass_filled.xml
deleted file mode 100644
index 7e80e61..0000000
--- a/app/src/main/res/drawable/hourglass_filled.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/mobile_vibrate.xml b/app/src/main/res/drawable/mobile_vibrate.xml
new file mode 100644
index 0000000..7435b8a
--- /dev/null
+++ b/app/src/main/res/drawable/mobile_vibrate.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/timer_filled.xml b/app/src/main/res/drawable/timer_filled.xml
new file mode 100644
index 0000000..9cfe048
--- /dev/null
+++ b/app/src/main/res/drawable/timer_filled.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/timer_outlined.xml b/app/src/main/res/drawable/timer_outlined.xml
new file mode 100644
index 0000000..17b99fb
--- /dev/null
+++ b/app/src/main/res/drawable/timer_outlined.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/fastlane/metadata/android/en-US/changelogs/7.txt b/fastlane/metadata/android/en-US/changelogs/7.txt
new file mode 100644
index 0000000..d57dc08
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/7.txt
@@ -0,0 +1,9 @@
+New features:
+- New custom alarm ringtone option
+- Vibration when a timer completes
+- New options in settings to disable alarm and vibration
+
+Fixes:
+- Alarm dialog not visible when on the stats or settings screen
+- Improved timer stability
+- System M3 colors not being used in Live Update/Now Bar progress bar
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index a296647..65c74b9 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index cd76715..e41b904 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index 0d34bd3..3785c3d 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
index 83c6065..9ce6efb 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
index 3ffb9e5..c5a3722 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
index 2d18d2a..77e28cc 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ