Merge pull request #32 from nsh07/foreground-service
feat: Implement a foreground service to run the timer
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.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_PROMOTED_NOTIFICATIONS" />
|
||||||
|
|
||||||
@@ -26,6 +28,13 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".service.TimerService"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Required to keep the timer running in the background" />
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.nsh07.pomodoro
|
package org.nsh07.pomodoro
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
import org.nsh07.pomodoro.data.AppContainer
|
import org.nsh07.pomodoro.data.AppContainer
|
||||||
import org.nsh07.pomodoro.data.DefaultAppContainer
|
import org.nsh07.pomodoro.data.DefaultAppContainer
|
||||||
|
|
||||||
@@ -9,5 +11,13 @@ class TomatoApplication : Application() {
|
|||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
container = DefaultAppContainer(this)
|
container = DefaultAppContainer(this)
|
||||||
|
|
||||||
|
val notificationChannel = NotificationChannel(
|
||||||
|
"timer",
|
||||||
|
"Timer progress",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
)
|
||||||
|
|
||||||
|
container.notificationManager.createNotificationChannel(notificationChannel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,26 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.data
|
package org.nsh07.pomodoro.data
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
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.service.addTimerActions
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToStr
|
||||||
|
|
||||||
interface AppContainer {
|
interface AppContainer {
|
||||||
val appPreferenceRepository: AppPreferenceRepository
|
val appPreferenceRepository: AppPreferenceRepository
|
||||||
val appStatRepository: AppStatRepository
|
val appStatRepository: AppStatRepository
|
||||||
val appTimerRepository: AppTimerRepository
|
val appTimerRepository: AppTimerRepository
|
||||||
|
val notificationManager: NotificationManagerCompat
|
||||||
|
val notificationBuilder: NotificationCompat.Builder
|
||||||
|
val timerState: MutableStateFlow<TimerState>
|
||||||
|
val time: MutableStateFlow<Long>
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultAppContainer(context: Context) : AppContainer {
|
class DefaultAppContainer(context: Context) : AppContainer {
|
||||||
@@ -27,4 +41,41 @@ class DefaultAppContainer(context: Context) : AppContainer {
|
|||||||
|
|
||||||
override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() }
|
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)
|
||||||
|
.setColor(Color.Red.toArgb())
|
||||||
|
.setContentIntent(
|
||||||
|
PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
context.packageManager.getLaunchIntentForPackage(context.packageName),
|
||||||
|
PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addTimerActions(context, R.drawable.play, "Start")
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setSilent(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setRequestPromotedOngoing(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val timerState: MutableStateFlow<TimerState> by lazy {
|
||||||
|
MutableStateFlow(
|
||||||
|
TimerState(
|
||||||
|
totalTime = appTimerRepository.focusTime,
|
||||||
|
timeStr = millisecondsToStr(appTimerRepository.focusTime),
|
||||||
|
nextTimeStr = millisecondsToStr(appTimerRepository.shortBreakTime)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val time: MutableStateFlow<Long> by lazy {
|
||||||
|
MutableStateFlow(appTimerRepository.focusTime)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
74
app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt
Normal file
74
app/src/main/java/org/nsh07/pomodoro/service/AddActions.kt
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.service
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
|
||||||
|
fun NotificationCompat.Builder.addTimerActions(
|
||||||
|
context: Context,
|
||||||
|
@DrawableRes playPauseIcon: Int,
|
||||||
|
playPauseText: String
|
||||||
|
): NotificationCompat.Builder = this
|
||||||
|
.addAction(
|
||||||
|
playPauseIcon,
|
||||||
|
playPauseText,
|
||||||
|
PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.TOGGLE.toString()
|
||||||
|
},
|
||||||
|
FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.restart,
|
||||||
|
"Reset",
|
||||||
|
PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.RESET.toString()
|
||||||
|
},
|
||||||
|
FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addAction(
|
||||||
|
R.drawable.skip_next,
|
||||||
|
"Skip",
|
||||||
|
PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.SKIP.toString()
|
||||||
|
},
|
||||||
|
FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun NotificationCompat.Builder.addStopAlarmAction(
|
||||||
|
context: Context
|
||||||
|
): NotificationCompat.Builder = this
|
||||||
|
.addAction(
|
||||||
|
R.drawable.alarm,
|
||||||
|
"Stop alarm",
|
||||||
|
PendingIntent.getService(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
Intent(context, TimerService::class.java).also {
|
||||||
|
it.action = TimerService.Actions.STOP_ALARM.toString()
|
||||||
|
},
|
||||||
|
FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
)
|
||||||
383
app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
Normal file
383
app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
package org.nsh07.pomodoro.service
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
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 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 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<TimerState>
|
||||||
|
private lateinit var _time: MutableStateFlow<Long>
|
||||||
|
|
||||||
|
val timeStateFlow by lazy {
|
||||||
|
_time.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
var time: Long
|
||||||
|
get() = timeStateFlow.value
|
||||||
|
set(value) = _time.update { value }
|
||||||
|
|
||||||
|
lateinit var timerState: StateFlow<TimerState>
|
||||||
|
|
||||||
|
private var cycles = 0
|
||||||
|
private var startTime = 0L
|
||||||
|
private var pauseTime = 0L
|
||||||
|
private var pauseDuration = 0L
|
||||||
|
|
||||||
|
private var job = SupervisorJob()
|
||||||
|
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
private val skipScope = CoroutineScope(Dispatchers.IO + job)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
_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) % 10
|
||||||
|
|
||||||
|
if (iterations == 0) showTimerNotification(time.toInt())
|
||||||
|
|
||||||
|
if (time < 0) {
|
||||||
|
skipTimer()
|
||||||
|
_timerState.update { currentState ->
|
||||||
|
currentState.copy(timerRunning = false)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
} 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
|
||||||
|
) {
|
||||||
|
if (complete) notificationBuilder.clearActions().addStopAlarmAction(this)
|
||||||
|
|
||||||
|
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
|
||||||
|
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..<timerRepository.sessionLength * 2) {
|
||||||
|
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()
|
||||||
|
).setColor(cs.tertiary.toArgb())
|
||||||
|
)
|
||||||
|
else it.addProgressSegment(
|
||||||
|
NotificationCompat.ProgressStyle.Segment(
|
||||||
|
timerRepository.longBreakTime.toInt()
|
||||||
|
).setColor(cs.tertiary.toArgb())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} 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()
|
||||||
|
} else (totalTime - remainingTime)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time
|
||||||
|
.setShortCriticalText(millisecondsToStr(time.coerceAtLeast(0)))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
if (complete) {
|
||||||
|
alarm.start()
|
||||||
|
_timerState.update { currentState ->
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
notificationBuilder.clearActions().addTimerActions(this, R.drawable.play, "Start next")
|
||||||
|
showTimerNotification(
|
||||||
|
when (timerState.value.timerMode) {
|
||||||
|
TimerMode.FOCUS -> timerRepository.focusTime.toInt()
|
||||||
|
TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt()
|
||||||
|
else -> timerRepository.longBreakTime.toInt()
|
||||||
|
},
|
||||||
|
paused = true,
|
||||||
|
complete = 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
runBlocking {
|
||||||
|
job.cancel()
|
||||||
|
saveTimeToDb()
|
||||||
|
notificationManager.cancel(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Actions {
|
||||||
|
TOGGLE, SKIP, RESET, STOP_ALARM
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.ContentTransform
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -33,6 +34,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
@@ -44,10 +46,12 @@ import androidx.navigation3.runtime.rememberNavBackStack
|
|||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.window.core.layout.WindowSizeClass
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
import org.nsh07.pomodoro.MainActivity.Companion.screens
|
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.settingsScreen.SettingsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@@ -57,6 +61,8 @@ fun AppScreen(
|
|||||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
||||||
statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
statsViewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
|
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
|
||||||
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle()
|
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -139,7 +145,33 @@ fun AppScreen(
|
|||||||
TimerScreen(
|
TimerScreen(
|
||||||
timerState = uiState,
|
timerState = uiState,
|
||||||
progress = { progress },
|
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(
|
modifier = modifier.padding(
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ fun TimerScreen(
|
|||||||
color = color,
|
color = color,
|
||||||
trackColor = colorContainer,
|
trackColor = colorContainer,
|
||||||
strokeWidth = 16.dp,
|
strokeWidth = 16.dp,
|
||||||
gapSize = 16.dp
|
gapSize = 8.dp
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
@@ -229,7 +229,7 @@ fun TimerScreen(
|
|||||||
cap = StrokeCap.Round,
|
cap = StrokeCap.Round,
|
||||||
),
|
),
|
||||||
wavelength = 60.dp,
|
wavelength = 60.dp,
|
||||||
gapSize = 16.dp
|
gapSize = 8.dp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
var expanded by remember { mutableStateOf(timerState.showBrandTitle) }
|
var expanded by remember { mutableStateOf(timerState.showBrandTitle) }
|
||||||
@@ -300,10 +300,10 @@ fun TimerScreen(
|
|||||||
{
|
{
|
||||||
FilledIconToggleButton(
|
FilledIconToggleButton(
|
||||||
onCheckedChange = { checked ->
|
onCheckedChange = { checked ->
|
||||||
|
onAction(TimerAction.ToggleTimer)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && checked) {
|
||||||
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
onAction(TimerAction.ToggleTimer)
|
|
||||||
},
|
},
|
||||||
checked = timerState.timerRunning,
|
checked = timerState.timerRunning,
|
||||||
colors = IconButtonDefaults.filledIconToggleButtonColors(
|
colors = IconButtonDefaults.filledIconToggleButtonColors(
|
||||||
|
|||||||
@@ -7,38 +7,22 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
package org.nsh07.pomodoro.ui.timerScreen.viewModel
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
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.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.AndroidViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
import androidx.lifecycle.application
|
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.lifecycle.viewmodel.initializer
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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.MainActivity
|
|
||||||
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
|
||||||
@@ -46,7 +30,6 @@ 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.text.Typography.middleDot
|
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
class TimerViewModel(
|
class TimerViewModel(
|
||||||
@@ -54,20 +37,10 @@ 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 _timerState: MutableStateFlow<TimerState>,
|
||||||
private val notificationManager: NotificationManagerCompat
|
private val _time: MutableStateFlow<Long>
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
private val _timerState = MutableStateFlow(
|
|
||||||
TimerState(
|
|
||||||
totalTime = timerRepository.focusTime,
|
|
||||||
timeStr = millisecondsToStr(timerRepository.focusTime),
|
|
||||||
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
|
val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
|
||||||
var timerJob: Job? = null
|
|
||||||
private val _time = MutableStateFlow(timerRepository.focusTime)
|
|
||||||
|
|
||||||
val time: StateFlow<Long> = _time.asStateFlow()
|
val time: StateFlow<Long> = _time.asStateFlow()
|
||||||
private var cycles = 0
|
private var cycles = 0
|
||||||
@@ -78,11 +51,6 @@ class TimerViewModel(
|
|||||||
|
|
||||||
private lateinit var cs: ColorScheme
|
private lateinit var cs: ColorScheme
|
||||||
|
|
||||||
private val alarm = MediaPlayer.create(
|
|
||||||
this.application,
|
|
||||||
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
timerRepository.focusTime =
|
timerRepository.focusTime =
|
||||||
@@ -132,16 +100,6 @@ class TimerViewModel(
|
|||||||
cs = colorScheme
|
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() {
|
private fun resetTimer() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
saveTimeToDb()
|
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() {
|
suspend fun saveTimeToDb() {
|
||||||
when (timerState.value.timerMode) {
|
when (timerState.value.timerMode) {
|
||||||
TimerMode.FOCUS -> statRepository
|
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..<timerRepository.sessionLength * 2) {
|
|
||||||
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()
|
|
||||||
).setColor(cs.tertiary.toArgb())
|
|
||||||
)
|
|
||||||
else it.addProgressSegment(
|
|
||||||
NotificationCompat.ProgressStyle.Segment(
|
|
||||||
timerRepository.longBreakTime.toInt()
|
|
||||||
).setColor(cs.tertiary.toArgb())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.setProgress( // Set the current progress by filling the previous intervals and part of the current interval
|
|
||||||
(totalTime - remainingTime) +
|
|
||||||
((cycles + 1) / 2) * timerRepository.focusTime.toInt() +
|
|
||||||
(cycles / 2) * timerRepository.shortBreakTime.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.setShowWhen(true)
|
|
||||||
.setWhen(System.currentTimeMillis() + remainingTime) // Sets the Live Activity/Now Bar chip time
|
|
||||||
.setSilent(true)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (complete) {
|
|
||||||
alarm.start()
|
|
||||||
_timerState.update { currentState ->
|
|
||||||
currentState.copy(alarmRinging = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun stopAlarm() {
|
|
||||||
alarm.pause()
|
|
||||||
alarm.seekTo(0)
|
|
||||||
_timerState.update { currentState ->
|
|
||||||
currentState.copy(alarmRinging = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
@@ -370,43 +140,16 @@ class TimerViewModel(
|
|||||||
val appPreferenceRepository = application.container.appPreferenceRepository
|
val appPreferenceRepository = application.container.appPreferenceRepository
|
||||||
val appStatRepository = application.container.appStatRepository
|
val appStatRepository = application.container.appStatRepository
|
||||||
val appTimerRepository = application.container.appTimerRepository
|
val appTimerRepository = application.container.appTimerRepository
|
||||||
|
val timerState = application.container.timerState
|
||||||
val notificationManager = NotificationManagerCompat.from(application)
|
val time = application.container.time
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
TimerViewModel(
|
TimerViewModel(
|
||||||
application = application,
|
application = application,
|
||||||
preferenceRepository = appPreferenceRepository,
|
preferenceRepository = appPreferenceRepository,
|
||||||
statRepository = appStatRepository,
|
statRepository = appStatRepository,
|
||||||
timerRepository = appTimerRepository,
|
timerRepository = appTimerRepository,
|
||||||
notificationBuilder = notificationBuilder,
|
_timerState = timerState,
|
||||||
notificationManager = notificationManager
|
_time = time
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user