feat: Implement a basic Progress-Centric notification
Permissions are not checked for now. This will be fixed in further commits
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".TomatoApplication"
|
android:name=".TomatoApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
|
|||||||
@@ -7,7 +7,15 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
|
import androidx.annotation.RequiresPermission
|
||||||
|
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.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
@@ -23,6 +31,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
import org.nsh07.pomodoro.data.PreferenceRepository
|
import org.nsh07.pomodoro.data.PreferenceRepository
|
||||||
import org.nsh07.pomodoro.data.Stat
|
import org.nsh07.pomodoro.data.Stat
|
||||||
@@ -30,12 +39,16 @@ import org.nsh07.pomodoro.data.StatRepository
|
|||||||
import org.nsh07.pomodoro.data.TimerRepository
|
import org.nsh07.pomodoro.data.TimerRepository
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToStr
|
import org.nsh07.pomodoro.utils.millisecondsToStr
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.text.Typography.middleDot
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
class TimerViewModel(
|
class TimerViewModel(
|
||||||
private val preferenceRepository: PreferenceRepository,
|
private val preferenceRepository: PreferenceRepository,
|
||||||
private val statRepository: StatRepository,
|
private val statRepository: StatRepository,
|
||||||
private val timerRepository: TimerRepository
|
private val timerRepository: TimerRepository,
|
||||||
|
private val notificationBuilder: NotificationCompat.Builder,
|
||||||
|
private val notificationManager: NotificationManagerCompat
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
// TODO: Document code
|
// TODO: Document code
|
||||||
private val _timerState = MutableStateFlow(
|
private val _timerState = MutableStateFlow(
|
||||||
@@ -134,6 +147,7 @@ class TimerViewModel(
|
|||||||
private fun skipTimer() {
|
private fun skipTimer() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveTimeToDb()
|
saveTimeToDb()
|
||||||
|
showTimerNotification(0, paused = true, complete = true)
|
||||||
startTime = 0L
|
startTime = 0L
|
||||||
pauseTime = 0L
|
pauseTime = 0L
|
||||||
pauseDuration = 0L
|
pauseDuration = 0L
|
||||||
@@ -174,6 +188,7 @@ class TimerViewModel(
|
|||||||
|
|
||||||
private fun toggleTimer() {
|
private fun toggleTimer() {
|
||||||
if (timerState.value.timerRunning) {
|
if (timerState.value.timerRunning) {
|
||||||
|
showTimerNotification(time.value.toInt(), paused = true)
|
||||||
_timerState.update { currentState ->
|
_timerState.update { currentState ->
|
||||||
currentState.copy(timerRunning = false)
|
currentState.copy(timerRunning = false)
|
||||||
}
|
}
|
||||||
@@ -183,6 +198,8 @@ class TimerViewModel(
|
|||||||
_timerState.update { it.copy(timerRunning = true) }
|
_timerState.update { it.copy(timerRunning = true) }
|
||||||
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
|
if (pauseTime != 0L) pauseDuration += SystemClock.elapsedRealtime() - pauseTime
|
||||||
|
|
||||||
|
var iterations = -1
|
||||||
|
|
||||||
timerJob = viewModelScope.launch {
|
timerJob = viewModelScope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!timerState.value.timerRunning) break
|
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) {
|
if (time.value < 0) {
|
||||||
skipTimer()
|
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..<timerRepository.sessionLength * 2) {
|
||||||
|
if (i % 2 == 0) it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.focusTime.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else if (i != (timerRepository.sessionLength * 2 - 1)) it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(timerRepository.shortBreakTime.toInt())
|
||||||
|
)
|
||||||
|
else it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.longBreakTime.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setProgress(
|
||||||
|
(totalTime - remainingTime) +
|
||||||
|
((cycles + 1) / 2) * timerRepository.focusTime.toInt() +
|
||||||
|
(cycles / 2) * timerRepository.shortBreakTime.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setShortCriticalText("${remainingTime / 60000} min")
|
||||||
|
.setSilent(true)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
@@ -240,10 +322,27 @@ class TimerViewModel(
|
|||||||
val appStatRepository = application.container.appStatRepository
|
val appStatRepository = application.container.appStatRepository
|
||||||
val appTimerRepository = application.container.appTimerRepository
|
val appTimerRepository = application.container.appTimerRepository
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(application)
|
||||||
|
|
||||||
|
val notificationChannel = NotificationChannel(
|
||||||
|
"timer",
|
||||||
|
"Timer progress",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(notificationChannel)
|
||||||
|
|
||||||
|
val notificationBuilder = NotificationCompat.Builder(application, "timer")
|
||||||
|
.setSmallIcon(R.drawable.hourglass_filled)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setColor(Color.Red.toArgb())
|
||||||
|
|
||||||
TimerViewModel(
|
TimerViewModel(
|
||||||
preferenceRepository = appPreferenceRepository,
|
preferenceRepository = appPreferenceRepository,
|
||||||
statRepository = appStatRepository,
|
statRepository = appStatRepository,
|
||||||
timerRepository = appTimerRepository
|
timerRepository = appTimerRepository,
|
||||||
|
notificationBuilder = notificationBuilder,
|
||||||
|
notificationManager = notificationManager
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user