Skip to content

Commit

Permalink
Merge pull request #55 from ahudson20/54-make-timer-persist-when-app-…
Browse files Browse the repository at this point in the history
…closed

Recreate timer when active/paused and app closed
  • Loading branch information
ahudson20 authored Apr 16, 2024
2 parents 4b91cad + 9a5fa0f commit 4ee1617
Show file tree
Hide file tree
Showing 17 changed files with 306 additions and 71 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/com/app/whakaara/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,14 @@ class MainActivity : ComponentActivity() {
}
}
}

override fun onResume() {
super.onResume()
viewModel.recreateTimer()
}

override fun onPause() {
super.onPause()
viewModel.saveTimerStateForRecreation()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import android.content.Context
import androidx.compose.ui.graphics.Color
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.app.whakaara.state.TimerStateDataStore
import com.app.whakaara.ui.theme.darkGreen
import com.app.whakaara.utils.GeneralUtils
import dagger.hilt.android.qualifiers.ApplicationContext
Expand All @@ -26,8 +29,13 @@ class PreferencesDataStore @Inject constructor(
private object PreferencesKeys {
val COLOUR_BACKGROUND_KEY = stringPreferencesKey("colour_background")
val COLOUR_TEXT_KEY = stringPreferencesKey("colour_text")
val TIMER_FINISH_KEY = longPreferencesKey("timer_finish")
val TIMER_ACTIVE_KEY = booleanPreferencesKey("timer_active")
val TIMER_PAUSED_KEY = booleanPreferencesKey("timer_paused")
val TIMER_TIME_STAMP = longPreferencesKey("timer_time_stamp")
}

//region colour
val readBackgroundColour = preferencesDataStore.data.map { preferences ->
preferences[PreferencesKeys.COLOUR_BACKGROUND_KEY] ?: GeneralUtils.convertColourToString(colour = darkGreen)
}
Expand All @@ -42,4 +50,25 @@ class PreferencesDataStore @Inject constructor(
preferences[PreferencesKeys.COLOUR_TEXT_KEY] = text
}
}
//endregion

//region timer
val readTimerStatus = preferencesDataStore.data.map { preferences ->
TimerStateDataStore(
remainingTimeInMillis = preferences[PreferencesKeys.TIMER_FINISH_KEY] ?: 0L,
isActive = preferences[PreferencesKeys.TIMER_ACTIVE_KEY] ?: false,
isPaused = preferences[PreferencesKeys.TIMER_PAUSED_KEY] ?: false,
timeStamp = preferences[PreferencesKeys.TIMER_TIME_STAMP] ?: 0L
)
}

suspend fun saveTimerData(state: TimerStateDataStore) {
preferencesDataStore.edit { preferences ->
preferences[PreferencesKeys.TIMER_FINISH_KEY] = state.remainingTimeInMillis
preferences[PreferencesKeys.TIMER_ACTIVE_KEY] = state.isActive
preferences[PreferencesKeys.TIMER_PAUSED_KEY] = state.isPaused
preferences[PreferencesKeys.TIMER_TIME_STAMP] = state.timeStamp
}
}
//endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ package com.app.whakaara.logic

import android.os.CountDownTimer
import com.app.whakaara.utils.constants.GeneralConstants.TIMER_INTERVAL
import javax.inject.Inject

class CountDownTimerUtil @Inject constructor() {
class CountDownTimerUtil {

private lateinit var timer: CountDownTimer

Expand All @@ -26,6 +25,6 @@ class CountDownTimerUtil @Inject constructor() {
}

fun cancel() {
timer.cancel()
if (::timer.isInitialized) timer.cancel()
}
}
48 changes: 46 additions & 2 deletions app/src/main/java/com/app/whakaara/logic/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.app.whakaara.data.alarm.Alarm
import com.app.whakaara.data.alarm.AlarmRepository
import com.app.whakaara.data.datastore.PreferencesDataStore
import com.app.whakaara.data.preferences.Preferences
import com.app.whakaara.data.preferences.PreferencesRepository
import com.app.whakaara.state.AlarmState
import com.app.whakaara.state.PreferencesState
import com.app.whakaara.state.StopwatchState
import com.app.whakaara.state.TimerState
import com.app.whakaara.state.TimerStateDataStore
import com.app.whakaara.utils.DateUtils.Companion.getAlarmTimeFormatted
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import java.util.Calendar
Expand All @@ -27,7 +30,8 @@ class MainViewModel @Inject constructor(
private val preferencesRepository: PreferencesRepository,
private val alarmManagerWrapper: AlarmManagerWrapper,
private val timerManagerWrapper: TimerManagerWrapper,
private val stopwatchManagerWrapper: StopwatchManagerWrapper
private val stopwatchManagerWrapper: StopwatchManagerWrapper,
private val preferencesDatastore: PreferencesDataStore
) : ViewModel() {

// alarm
Expand Down Expand Up @@ -230,12 +234,52 @@ class MainViewModel @Inject constructor(
timerManagerWrapper.pauseTimer()
}

fun resetTimer() {
fun resetTimer() = viewModelScope.launch {
timerManagerWrapper.resetTimer()
preferencesDatastore.saveTimerData(
TimerStateDataStore(
remainingTimeInMillis = 0L,
isActive = false,
isPaused = false
)
)
}

fun restartTimer() {
timerManagerWrapper.restartTimer()
}

fun recreateTimer() = viewModelScope.launch(Dispatchers.Main) {
val status = preferencesDatastore.readTimerStatus.first()
val difference = System.currentTimeMillis() - status.timeStamp
if (status.remainingTimeInMillis > 0 && timerState.value.isStart && (status.remainingTimeInMillis > difference)) {
if (status.isActive) {
timerManagerWrapper.recreateActiveTimer(
milliseconds = status.remainingTimeInMillis - difference
)
} else if (status.isPaused) {
timerManagerWrapper.recreatePausedTimer(
milliseconds = status.remainingTimeInMillis - difference
)
}

preferencesDatastore.saveTimerData(
state = TimerStateDataStore()
)
}
}

fun saveTimerStateForRecreation() = viewModelScope.launch(Dispatchers.IO) {
if (!timerState.value.isStart) {
preferencesDatastore.saveTimerData(
TimerStateDataStore(
remainingTimeInMillis = timerState.value.currentTime,
isActive = timerState.value.isTimerActive,
isPaused = timerState.value.isTimerPaused,
timeStamp = System.currentTimeMillis()
)
)
}
}
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ import com.app.whakaara.utils.constants.NotificationUtilsConstants.STOPWATCH_REC
import com.app.whakaara.utils.constants.NotificationUtilsConstants.STOPWATCH_RECEIVER_ACTION_START
import com.app.whakaara.utils.constants.NotificationUtilsConstants.STOPWATCH_RECEIVER_ACTION_STOP
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
Expand All @@ -34,9 +33,9 @@ class StopwatchManagerWrapper @Inject constructor(
private val app: Application,
private val notificationManager: NotificationManager,
@Named("stopwatch")
private val stopwatchNotificationBuilder: NotificationCompat.Builder
private val stopwatchNotificationBuilder: NotificationCompat.Builder,
private val coroutineScope: CoroutineScope
) {
private var coroutineScope = CoroutineScope(Dispatchers.Main)
val stopwatchState = MutableStateFlow(StopwatchState())

fun startStopwatch() {
Expand Down Expand Up @@ -88,8 +87,7 @@ class StopwatchManagerWrapper @Inject constructor(
}

fun resetStopwatch() {
coroutineScope.cancel()
coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.coroutineContext.cancelChildren()
cancelNotification()
stopwatchState.update {
it.copy(
Expand Down
67 changes: 66 additions & 1 deletion app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import com.app.whakaara.R
import com.app.whakaara.data.datastore.PreferencesDataStore
import com.app.whakaara.receiver.TimerReceiver
import com.app.whakaara.service.MediaPlayerService
import com.app.whakaara.state.TimerState
import com.app.whakaara.state.TimerStateDataStore
import com.app.whakaara.utils.DateUtils
import com.app.whakaara.utils.PendingIntentUtils
import com.app.whakaara.utils.constants.DateUtilsConstants.TIMER_INPUT_INITIAL_VALUE
Expand All @@ -30,19 +33,26 @@ import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_NOTIFIC
import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_RECEIVER_ACTION_PAUSE
import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_RECEIVER_ACTION_START
import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_RECEIVER_ACTION_STOP
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.Calendar
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.cancellation.CancellationException

class TimerManagerWrapper @Inject constructor(
private val app: Application,
private val alarmManager: AlarmManager,
private val notificationManager: NotificationManager,
@Named("timer")
private val timerNotificationBuilder: NotificationCompat.Builder,
private val countDownTimerUtil: CountDownTimerUtil
private val countDownTimerUtil: CountDownTimerUtil,
private val preferencesDatastore: PreferencesDataStore,
private val coroutineScope: CoroutineScope
) {
val timerState = MutableStateFlow(TimerState())

Expand Down Expand Up @@ -70,6 +80,24 @@ class TimerManagerWrapper @Inject constructor(
}
}

fun recreateActiveTimer(
milliseconds: Long
) {
countDownTimerUtil.cancel()
startCountDownTimer(timeToCountDown = milliseconds)
timerState.update {
it.copy(
isTimerPaused = false,
isStart = false,
isTimerActive = true,
millisecondsFromTimerInput = milliseconds,
inputHours = TimeUnit.MILLISECONDS.toHours(milliseconds).toString(),
inputMinutes = TimeUnit.MILLISECONDS.toMinutes(milliseconds).toString(),
inputSeconds = TimeUnit.MILLISECONDS.toSeconds(milliseconds).toString()
)
}
}

fun startTimer() {
val currentTimeInMillis = Calendar.getInstance().timeInMillis
if (timerState.value.isTimerPaused) {
Expand Down Expand Up @@ -137,10 +165,28 @@ class TimerManagerWrapper @Inject constructor(
millisecondsFromTimerInput = ZERO_MILLIS
)
}
resetTimerStateDataStore()
}
)
}

fun recreatePausedTimer(
milliseconds: Long
) {
timerState.update {
it.copy(
isStart = false,
isTimerActive = false,
isTimerPaused = true,
currentTime = milliseconds,
millisecondsFromTimerInput = milliseconds,
time = DateUtils.formatTimeForTimer(
millis = milliseconds
)
)
}
}

fun pauseTimer() {
if (!timerState.value.isTimerPaused) {
cancelTimerAlarm()
Expand Down Expand Up @@ -173,6 +219,7 @@ class TimerManagerWrapper @Inject constructor(
millisecondsFromTimerInput = ZERO_MILLIS
)
}
resetTimerStateDataStore()
}

fun restartTimer() {
Expand Down Expand Up @@ -321,6 +368,24 @@ class TimerManagerWrapper @Inject constructor(
notificationManager.cancel(TIMER_NOTIFICATION_ID)
}

private fun resetTimerStateDataStore() {
try {
coroutineScope.launch {
preferencesDatastore.saveTimerData(
state = TimerStateDataStore()
)
}
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
Log.e("resetTimerStateDataStoreTAG", "resetTimerStateDataStore execution failed", t)
} finally {
// Nothing can be in the `finally` block after this, as this throws a
// `CancellationException`
coroutineScope.cancel()
}
}

companion object {
fun Context.getTimerReceiverIntent(intentAction: String): Intent {
return Intent(this, TimerReceiver::class.java).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.app.whakaara.module

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import javax.inject.Qualifier

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class DefaultDispatcher

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class IoDispatcher

@Retention(AnnotationRetention.RUNTIME)
@Qualifier
annotation class MainDispatcher

@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class MainImmediateDispatcher

@InstallIn(SingletonComponent::class)
@Module
object CoroutinesDispatchersModule {

@DefaultDispatcher
@Provides
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default

@IoDispatcher
@Provides
fun providesIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

@MainDispatcher
@Provides
fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main

@MainImmediateDispatcher
@Provides
fun providesMainImmediateDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}
Loading

0 comments on commit 4ee1617

Please sign in to comment.