diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9dc04d4..a5372a3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
@@ -26,6 +28,13 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
index b0e144f..e1902bc 100644
--- a/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/TomatoApplication.kt
@@ -1,6 +1,8 @@
package org.nsh07.pomodoro
import android.app.Application
+import android.app.NotificationChannel
+import android.app.NotificationManager
import org.nsh07.pomodoro.data.AppContainer
import org.nsh07.pomodoro.data.DefaultAppContainer
@@ -9,5 +11,13 @@ class TomatoApplication : Application() {
override fun onCreate() {
super.onCreate()
container = DefaultAppContainer(this)
+
+ val notificationChannel = NotificationChannel(
+ "timer",
+ "Timer progress",
+ NotificationManager.IMPORTANCE_HIGH
+ )
+
+ container.notificationManager.createNotificationChannel(notificationChannel)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
index 2d45910..51c3570 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
@@ -8,11 +8,23 @@
package org.nsh07.pomodoro.data
import android.content.Context
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.nsh07.pomodoro.R
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
+import org.nsh07.pomodoro.utils.millisecondsToStr
interface AppContainer {
val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository
+ val notificationManager: NotificationManagerCompat
+ val notificationBuilder: NotificationCompat.Builder
+ val timerState: MutableStateFlow
+ val time: MutableStateFlow
}
class DefaultAppContainer(context: Context) : AppContainer {
@@ -27,4 +39,31 @@ class DefaultAppContainer(context: Context) : AppContainer {
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
+ override val notificationManager: NotificationManagerCompat by lazy {
+ NotificationManagerCompat.from(context)
+ }
+
+ override val notificationBuilder: NotificationCompat.Builder by lazy {
+ NotificationCompat.Builder(context, "timer")
+ .setSmallIcon(R.drawable.tomato_logo_notification)
+ .setOngoing(true)
+ .setColor(Color.Red.toArgb())
+ .setRequestPromotedOngoing(true)
+ .setOngoing(true)
+ }
+
+ override val timerState: MutableStateFlow by lazy {
+ MutableStateFlow(
+ TimerState(
+ totalTime = appTimerRepository.focusTime,
+ timeStr = millisecondsToStr(appTimerRepository.focusTime),
+ nextTimeStr = millisecondsToStr(appTimerRepository.shortBreakTime)
+ )
+ )
+ }
+
+ override val time: MutableStateFlow by lazy {
+ MutableStateFlow(appTimerRepository.focusTime)
+ }
+
}
\ 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
new file mode 100644
index 0000000..9e94929
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
@@ -0,0 +1,346 @@
+package org.nsh07.pomodoro.service
+
+import android.annotation.SuppressLint
+import android.app.Service
+import android.content.Intent
+import android.media.MediaPlayer
+import android.os.IBinder
+import android.os.SystemClock
+import android.provider.Settings
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.material3.ColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+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 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
+
+@ExperimentalAnimationApi
+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()
+ }
+
+ var time: Long
+ get() = timeStateFlow.value
+ set(value) = _time.update { value }
+
+ lateinit var timerState: StateFlow
+
+ private var cycles = 0
+ private var startTime = 0L
+ private var pauseTime = 0L
+ private var pauseDuration = 0L
+
+ private val timerJob = SupervisorJob()
+ private val scope = CoroutineScope(Dispatchers.IO + timerJob)
+ private val skipScope = CoroutineScope(Dispatchers.IO)
+
+ private lateinit var alarm: MediaPlayer
+
+ private var cs: ColorScheme = lightColorScheme()
+
+ 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
+ )
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ when (intent?.action) {
+ Actions.TOGGLE.toString() -> {
+ startForegroundService()
+ toggleTimer()
+ }
+
+ Actions.RESET.toString() -> {
+ if (timerState.value.timerRunning) toggleTimer()
+ resetTimer()
+ stopForegroundService()
+ }
+
+ Actions.SKIP.toString() -> skipTimer(true)
+
+ Actions.STOP_ALARM.toString() -> stopAlarm()
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ private fun toggleTimer() {
+ if (timerState.value.timerRunning) {
+ showTimerNotification(time.toInt(), paused = true)
+ _timerState.update { currentState ->
+ currentState.copy(timerRunning = false)
+ }
+ pauseTime = SystemClock.elapsedRealtime()
+ } else {
+ _timerState.update { it.copy(timerRunning = true) }
+ if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
+
+ var iterations = -1
+
+ scope.launch {
+ while (true) {
+ if (!timerState.value.timerRunning) break
+ if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
+
+ time = when (timerState.value.timerMode) {
+ TimerMode.FOCUS -> timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
+
+ TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
+
+ else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
+ }
+
+ iterations = (iterations + 1) % 50
+
+ if (iterations == 0) showTimerNotification(time.toInt())
+
+ if (time < 0) {
+ skipTimer()
+
+ _timerState.update { currentState ->
+ currentState.copy(timerRunning = false)
+ }
+ timerJob.cancel()
+ } else {
+ _timerState.update { currentState ->
+ currentState.copy(
+ timeStr = millisecondsToStr(time)
+ )
+ }
+ }
+
+ delay(100)
+ }
+ }
+ }
+ }
+
+ @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI
+ 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"
+ }
+
+ val remainingTimeString = if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1"
+ else (remainingTime.toFloat() / 60000f).toInt()
+
+ notificationManager.notify(
+ 1,
+ notificationBuilder
+ .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().also {
+ // Add all the Focus, Short break and long break intervals in order
+ for (i in 0..
+ currentState.copy(alarmRinging = true)
+ }
+ }
+ }
+
+ private fun resetTimer() {
+ skipScope.launch {
+ saveTimeToDb()
+ time = timerRepository.focusTime
+ cycles = 0
+ startTime = 0L
+ pauseTime = 0L
+ pauseDuration = 0L
+
+ _timerState.update { currentState ->
+ currentState.copy(
+ timerMode = TimerMode.FOCUS,
+ timeStr = millisecondsToStr(time),
+ totalTime = time,
+ nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
+ nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime),
+ currentFocusCount = 1,
+ totalFocusCount = timerRepository.sessionLength
+ )
+ }
+ }
+ }
+
+ private fun skipTimer(fromButton: Boolean = false) {
+ skipScope.launch {
+ saveTimeToDb()
+ showTimerNotification(0, paused = true, complete = !fromButton)
+ startTime = 0L
+ pauseTime = 0L
+ pauseDuration = 0L
+
+ cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
+
+ if (cycles % 2 == 0) {
+ time = timerRepository.focusTime
+ _timerState.update { currentState ->
+ currentState.copy(
+ timerMode = TimerMode.FOCUS,
+ timeStr = millisecondsToStr(time),
+ totalTime = time,
+ nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
+ nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
+ timerRepository.longBreakTime
+ ) else millisecondsToStr(
+ timerRepository.shortBreakTime
+ ),
+ currentFocusCount = cycles / 2 + 1,
+ totalFocusCount = timerRepository.sessionLength
+ )
+ }
+ } else {
+ val long = cycles == (timerRepository.sessionLength * 2) - 1
+ time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime
+
+ _timerState.update { currentState ->
+ currentState.copy(
+ timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
+ timeStr = millisecondsToStr(time),
+ totalTime = time,
+ nextTimerMode = TimerMode.FOCUS,
+ nextTimeStr = millisecondsToStr(timerRepository.focusTime)
+ )
+ }
+ }
+ }
+ }
+
+ fun stopAlarm() {
+ alarm.pause()
+ alarm.seekTo(0)
+ _timerState.update { currentState ->
+ currentState.copy(alarmRinging = false)
+ }
+ }
+
+ suspend fun saveTimeToDb() {
+ when (timerState.value.timerMode) {
+ TimerMode.FOCUS -> statRepository.addFocusTime(
+ (timerState.value.totalTime - time).coerceAtLeast(
+ 0L
+ )
+ )
+
+ else -> statRepository.addBreakTime((timerState.value.totalTime - time).coerceAtLeast(0L))
+ }
+ }
+
+ private fun startForegroundService() {
+ startForeground(1, notificationBuilder.build())
+ }
+
+ private fun stopForegroundService() {
+ notificationManager.cancel(1)
+ stopForeground(STOP_FOREGROUND_REMOVE)
+ stopSelf()
+ }
+
+ private fun setStopButton() {
+ // TODO
+ }
+
+ private fun setResumeButton() {
+ // TODO
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ timerJob.cancel()
+ }
+
+ enum class Actions {
+ TOGGLE, SKIP, RESET, STOP_ALARM
+ }
+}
\ 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 dadfe37..24914b1 100644
--- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
@@ -7,8 +7,10 @@
package org.nsh07.pomodoro.ui
+import android.content.Intent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.Crossfade
+import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
@@ -33,6 +35,7 @@ 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
@@ -44,19 +47,26 @@ import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.MainActivity.Companion.screens
+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.TimerScreen
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
-@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
+@OptIn(
+ ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class,
+ ExperimentalAnimationApi::class
+)
@Composable
fun AppScreen(
modifier: Modifier = Modifier,
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) {
+ val context = LocalContext.current
+
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle()
@@ -139,7 +149,33 @@ fun AppScreen(
TimerScreen(
timerState = uiState,
progress = { progress },
- onAction = timerViewModel::onAction,
+ onAction = { action ->
+ when (action) {
+ TimerAction.ResetTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.RESET.toString()
+ context.startService(it)
+ }
+
+ is TimerAction.SkipTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.SKIP.toString()
+ context.startService(it)
+ }
+
+ TimerAction.StopAlarm ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.STOP_ALARM.toString()
+ context.startService(it)
+ }
+
+ TimerAction.ToggleTimer ->
+ Intent(context, TimerService::class.java).also {
+ it.action = TimerService.Actions.TOGGLE.toString()
+ context.startService(it)
+ }
+ }
+ },
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
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 ebfd45e..a173dd4 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,38 +7,22 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel
-import android.annotation.SuppressLint
import android.app.Application
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Intent
-import android.media.MediaPlayer
-import android.os.SystemClock
-import android.provider.Settings
import androidx.compose.material3.ColorScheme
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
-import androidx.core.app.NotificationCompat
-import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
-import androidx.lifecycle.application
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.Job
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 org.nsh07.pomodoro.MainActivity
-import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.Stat
@@ -46,7 +30,6 @@ import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate
-import kotlin.text.Typography.middleDot
@OptIn(FlowPreview::class)
class TimerViewModel(
@@ -54,20 +37,10 @@ class TimerViewModel(
private val preferenceRepository: PreferenceRepository,
private val statRepository: StatRepository,
private val timerRepository: TimerRepository,
- private val notificationBuilder: NotificationCompat.Builder,
- private val notificationManager: NotificationManagerCompat
+ private val _timerState: MutableStateFlow,
+ private val _time: MutableStateFlow
) : AndroidViewModel(application) {
- private val _timerState = MutableStateFlow(
- TimerState(
- totalTime = timerRepository.focusTime,
- timeStr = millisecondsToStr(timerRepository.focusTime),
- nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
- )
- )
-
val timerState: StateFlow = _timerState.asStateFlow()
- var timerJob: Job? = null
- private val _time = MutableStateFlow(timerRepository.focusTime)
val time: StateFlow = _time.asStateFlow()
private var cycles = 0
@@ -78,11 +51,6 @@ class TimerViewModel(
private lateinit var cs: ColorScheme
- private val alarm = MediaPlayer.create(
- this.application,
- Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
- )
-
init {
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime =
@@ -132,16 +100,6 @@ class TimerViewModel(
cs = colorScheme
}
- fun onAction(action: TimerAction) {
- when (action) {
- is TimerAction.SkipTimer -> skipTimer(action.fromButton)
-
- TimerAction.ResetTimer -> resetTimer()
- TimerAction.StopAlarm -> stopAlarm()
- TimerAction.ToggleTimer -> toggleTimer()
- }
- }
-
private fun resetTimer() {
viewModelScope.launch {
saveTimeToDb()
@@ -165,107 +123,6 @@ class TimerViewModel(
}
}
- private fun skipTimer(fromButton: Boolean = false) {
- viewModelScope.launch {
- saveTimeToDb()
- showTimerNotification(0, paused = true, complete = !fromButton)
- startTime = 0L
- pauseTime = 0L
- pauseDuration = 0L
-
- cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
-
- if (cycles % 2 == 0) {
- _time.update { timerRepository.focusTime }
- _timerState.update { currentState ->
- currentState.copy(
- timerMode = TimerMode.FOCUS,
- timeStr = millisecondsToStr(time.value),
- totalTime = time.value,
- nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
- nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr(
- timerRepository.longBreakTime
- ) else millisecondsToStr(
- timerRepository.shortBreakTime
- ),
- currentFocusCount = cycles / 2 + 1,
- totalFocusCount = timerRepository.sessionLength
- )
- }
- } else {
- val long = cycles == (timerRepository.sessionLength * 2) - 1
- _time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime }
-
- _timerState.update { currentState ->
- currentState.copy(
- timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
- timeStr = millisecondsToStr(time.value),
- totalTime = time.value,
- nextTimerMode = TimerMode.FOCUS,
- nextTimeStr = millisecondsToStr(timerRepository.focusTime)
- )
- }
- }
- }
- }
-
- private fun toggleTimer() {
- if (timerState.value.timerRunning) {
- showTimerNotification(time.value.toInt(), paused = true)
- _timerState.update { currentState ->
- currentState.copy(timerRunning = false)
- }
- timerJob?.cancel()
- pauseTime = SystemClock.elapsedRealtime()
- } else {
- _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
- if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
-
- _time.update {
- when (timerState.value.timerMode) {
- TimerMode.FOCUS ->
- timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
-
- TimerMode.SHORT_BREAK ->
- timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
-
- else ->
- timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration).toInt()
- }
- }
-
- iterations = (iterations + 1) % 50
-
- if (iterations == 0) showTimerNotification(time.value.toInt())
-
- if (time.value < 0) {
- skipTimer()
-
- _timerState.update { currentState ->
- currentState.copy(timerRunning = false)
- }
- timerJob?.cancel()
- } else {
- _timerState.update { currentState ->
- currentState.copy(
- timeStr = millisecondsToStr(time.value)
- )
- }
- }
-
- delay(100)
- }
- }
- }
- }
-
suspend fun saveTimeToDb() {
when (timerState.value.timerMode) {
TimerMode.FOCUS -> statRepository
@@ -276,93 +133,6 @@ class TimerViewModel(
}
}
- @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI
- 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"
- }
-
- val remainingTimeString =
- if ((remainingTime.toFloat() / 60000f) < 1.0f) "< 1"
- else (remainingTime.toFloat() / 60000f).toInt()
-
- notificationManager.notify(
- 1,
- notificationBuilder
- .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()
- .also {
- // Add all the Focus, Short break and long break intervals in order
- for (i in 0..
- currentState.copy(alarmRinging = true)
- }
- }
- }
-
- fun stopAlarm() {
- alarm.pause()
- alarm.seekTo(0)
- _timerState.update { currentState ->
- currentState.copy(alarmRinging = false)
- }
- }
-
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
@@ -370,43 +140,16 @@ class TimerViewModel(
val appPreferenceRepository = application.container.appPreferenceRepository
val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository
-
- val notificationManager = NotificationManagerCompat.from(application)
-
- val notificationChannel = NotificationChannel(
- "timer",
- "Timer progress",
- 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.tomato_logo_notification)
- .setOngoing(true)
- .setColor(Color.Red.toArgb())
- .setContentIntent(contentIntent)
- .setRequestPromotedOngoing(true)
- .setOngoing(true)
+ val timerState = application.container.timerState
+ val time = application.container.time
TimerViewModel(
application = application,
preferenceRepository = appPreferenceRepository,
statRepository = appStatRepository,
timerRepository = appTimerRepository,
- notificationBuilder = notificationBuilder,
- notificationManager = notificationManager
+ _timerState = timerState,
+ _time = time
)
}
}