From 19c47dd69716a87c7d3768b84b8733dfce240a8e Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 20 Aug 2025 19:52:45 +0530 Subject: [PATCH 1/7] feat: Implement a basic Progress-Centric notification Permissions are not checked for now. This will be fixed in further commits --- app/src/main/AndroidManifest.xml | 2 + .../timerScreen/viewModel/TimerViewModel.kt | 103 +++++++++++++++++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0e52b75..fdc33c7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + currentState.copy(timerRunning = false) } @@ -183,6 +198,8 @@ class TimerViewModel( _timerState.update { it.copy(timerRunning = true) } if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime + var iterations = -1 + timerJob = viewModelScope.launch { while (true) { if (!timerState.value.timerRunning) break @@ -201,6 +218,10 @@ class TimerViewModel( } } + iterations = (iterations + 1) % 50 + + if (iterations == 0) showTimerNotification(time.value.toInt()) + if (time.value < 0) { skipTimer() @@ -232,6 +253,67 @@ class TimerViewModel( } } + @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) + fun showTimerNotification( + remainingTime: Int, + paused: Boolean = false, + complete: Boolean = false + ) { + val totalTime = when (timerState.value.timerMode) { + TimerMode.FOCUS -> timerRepository.focusTime.toInt() + TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() + else -> timerRepository.longBreakTime.toInt() + } + + val currentTimer = when (timerState.value.timerMode) { + TimerMode.FOCUS -> "Focus" + TimerMode.SHORT_BREAK -> "Short break" + else -> "Long break" + } + + val nextTimer = when (timerState.value.nextTimerMode) { + TimerMode.FOCUS -> "Focus" + TimerMode.SHORT_BREAK -> "Short break" + else -> "Long break" + } + + notificationManager.notify( + 1, + notificationBuilder + .setContentTitle("$currentTimer $middleDot ${ceil(remainingTime.toFloat() / 60000f).toInt()} min remaining") + .setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})") + .setStyle( + NotificationCompat.ProgressStyle() + .also { + for (i in 0.. Date: Thu, 21 Aug 2025 12:37:06 +0530 Subject: [PATCH 2/7] feat: Open app on notification click Timer control buttons will be added in the next commit --- .../timerScreen/viewModel/TimerViewModel.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) 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 f849640..1f000d6 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 @@ -10,6 +10,8 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent import android.os.SystemClock import androidx.annotation.RequiresPermission import androidx.compose.ui.graphics.Color @@ -31,6 +33,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.data.PreferenceRepository @@ -277,10 +280,18 @@ class TimerViewModel( else -> "Long break" } + val remainingTimeString = + if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1" + else ceil(remainingTime.toFloat() / 60000f).toInt() + notificationManager.notify( 1, notificationBuilder - .setContentTitle("$currentTimer $middleDot ${ceil(remainingTime.toFloat() / 60000f).toInt()} min remaining") + .setContentTitle( + if (!complete) { + "$currentTimer $middleDot $remainingTimeString min remaining" + if (paused) " $middleDot Paused" else "" + } else "$currentTimer $middleDot Completed" + ) .setContentText("Up next: $nextTimer (${timerState.value.nextTimeStr})") .setStyle( NotificationCompat.ProgressStyle() @@ -327,15 +338,27 @@ class TimerViewModel( val notificationChannel = NotificationChannel( "timer", "Timer progress", - NotificationManager.IMPORTANCE_DEFAULT + NotificationManager.IMPORTANCE_HIGH ) notificationManager.createNotificationChannel(notificationChannel) + val openAppIntent = Intent(application, MainActivity::class.java) + openAppIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + openAppIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + + val contentIntent = PendingIntent.getActivity( + application, + System.currentTimeMillis().toInt(), + openAppIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val notificationBuilder = NotificationCompat.Builder(application, "timer") .setSmallIcon(R.drawable.hourglass_filled) .setOngoing(true) .setColor(Color.Red.toArgb()) + .setContentIntent(contentIntent) TimerViewModel( preferenceRepository = appPreferenceRepository, From 7508465a425c7c22d28fde002d0948e0d9548c24 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 21 Aug 2025 13:05:03 +0530 Subject: [PATCH 3/7] feat: Make notification a live update notification --- app/src/main/AndroidManifest.xml | 1 + .../pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fdc33c7..9dc04d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + Date: Thu, 21 Aug 2025 18:04:19 +0530 Subject: [PATCH 4/7] feat: Update timer icon --- .../timerScreen/viewModel/TimerViewModel.kt | 2 +- .../res/drawable/tomato_logo_notification.xml | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/tomato_logo_notification.xml 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 dbfa130..877106e 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 @@ -354,7 +354,7 @@ class TimerViewModel( ) val notificationBuilder = NotificationCompat.Builder(application, "timer") - .setSmallIcon(R.drawable.hourglass_filled) + .setSmallIcon(R.drawable.tomato_logo_notification) .setOngoing(true) .setColor(Color.Red.toArgb()) .setContentIntent(contentIntent) diff --git a/app/src/main/res/drawable/tomato_logo_notification.xml b/app/src/main/res/drawable/tomato_logo_notification.xml new file mode 100644 index 0000000..1410962 --- /dev/null +++ b/app/src/main/res/drawable/tomato_logo_notification.xml @@ -0,0 +1,25 @@ + + + + + + From d37dde69c60ae5a02d04dd286d07022850898540 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 21 Aug 2025 23:06:58 +0530 Subject: [PATCH 5/7] feat: Set segment colors for progress segments in notification --- .../main/java/org/nsh07/pomodoro/MainActivity.kt | 2 ++ .../ui/timerScreen/viewModel/TimerViewModel.kt | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 9e10f0f..f55cb02 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.compose.material3.MaterialTheme.colorScheme import org.nsh07.pomodoro.ui.AppScreen import org.nsh07.pomodoro.ui.NavItem import org.nsh07.pomodoro.ui.Screen @@ -22,6 +23,7 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { TomatoTheme { + timerViewModel.setCompositionLocals(colorScheme) AppScreen(timerViewModel = timerViewModel, statsViewModel = statsViewModel) } } 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 877106e..63b46f7 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 @@ -14,6 +14,7 @@ import android.app.PendingIntent import android.content.Intent import android.os.SystemClock import androidx.annotation.RequiresPermission +import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.core.app.NotificationCompat @@ -72,6 +73,8 @@ class TimerViewModel( private var pauseTime = 0L private var pauseDuration = 0L + private lateinit var cs: ColorScheme + init { viewModelScope.launch(Dispatchers.IO) { timerRepository.focusTime = @@ -117,6 +120,10 @@ class TimerViewModel( } } + fun setCompositionLocals(colorScheme: ColorScheme) { + cs = colorScheme + } + fun onAction(action: TimerAction) { when (action) { TimerAction.ResetTimer -> resetTimer() @@ -299,15 +306,17 @@ class TimerViewModel( if (i % 2 == 0) it.addProgressSegment( NotificationCompat.ProgressStyle.Segment( timerRepository.focusTime.toInt() - ) + ).setColor(cs.primary.toArgb()) ) else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment( - NotificationCompat.ProgressStyle.Segment(timerRepository.shortBreakTime.toInt()) + NotificationCompat.ProgressStyle.Segment( + timerRepository.shortBreakTime.toInt() + ).setColor(cs.tertiary.toArgb()) ) else it.addProgressSegment( NotificationCompat.ProgressStyle.Segment( timerRepository.longBreakTime.toInt() - ) + ).setColor(cs.tertiary.toArgb()) ) } } From 68e70e9ba846b6dc0f3aa26674575d1742761da8 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sat, 23 Aug 2025 08:51:35 +0530 Subject: [PATCH 6/7] feat: Ask notification permission --- .../nsh07/pomodoro/ui/timerScreen/TimerScreen.kt | 16 +++++++++++++++- .../ui/timerScreen/viewModel/TimerViewModel.kt | 11 +++++------ 2 files changed, 20 insertions(+), 7 deletions(-) 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 d731065..4c492c1 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 @@ -7,6 +7,10 @@ package org.nsh07.pomodoro.ui.timerScreen +import android.Manifest +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent import androidx.compose.animation.animateColorAsState import androidx.compose.animation.slideInVertically @@ -90,6 +94,11 @@ fun TimerScreen( animationSpec = motionScheme.slowEffectsSpec() ) + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + onResult = {} + ) + Column(modifier = modifier) { TopAppBar( title = { @@ -250,7 +259,12 @@ fun TimerScreen( customItem( { FilledIconToggleButton( - onCheckedChange = { onAction(TimerAction.ToggleTimer) }, + onCheckedChange = { checked -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) { + permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } + onAction(TimerAction.ToggleTimer) + }, checked = timerState.timerRunning, colors = IconButtonDefaults.filledIconToggleButtonColors( checkedContainerColor = color, 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 63b46f7..71feece 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 @@ -7,13 +7,12 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel -import android.Manifest +import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.content.Intent import android.os.SystemClock -import androidx.annotation.RequiresPermission import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb @@ -53,7 +52,6 @@ class TimerViewModel( private val notificationBuilder: NotificationCompat.Builder, private val notificationManager: NotificationManagerCompat ) : ViewModel() { - // TODO: Document code private val _timerState = MutableStateFlow( TimerState( totalTime = timerRepository.focusTime, @@ -262,7 +260,7 @@ class TimerViewModel( } } - @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS) + @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI fun showTimerNotification( remainingTime: Int, paused: Boolean = false, @@ -302,6 +300,7 @@ class TimerViewModel( .setStyle( NotificationCompat.ProgressStyle() .also { + // Add all the Focus, Short break and long break intervals in order for (i in 0.. Date: Sat, 23 Aug 2025 10:58:23 +0530 Subject: [PATCH 7/7] feat: Add a notification sound when timer is complete #15 --- .../nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 71feece..74cc8ac 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 @@ -327,7 +327,7 @@ class TimerViewModel( ) .setShowWhen(true) .setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time - .setSilent(true) + .setSilent(!complete) .build() ) }