From 39dbaf181eb313b2bd51bc545d541c7eff03c5d3 Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 10:48:54 +0100 Subject: [PATCH 1/7] recreate timer --- .../app/whakaara/activities/MainActivity.kt | 10 +++ .../data/datastore/PreferencesDataStore.kt | 29 +++++++++ .../app/whakaara/logic/CountDownTimerUtil.kt | 5 +- .../com/app/whakaara/logic/MainViewModel.kt | 62 ++++++++++++++++++- .../app/whakaara/logic/TimerManagerWrapper.kt | 51 ++++++++++++++- .../app/whakaara/module/NotificationModule.kt | 6 +- .../java/com/app/whakaara/state/TimerState.kt | 7 +++ 7 files changed, 162 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/app/whakaara/activities/MainActivity.kt b/app/src/main/java/com/app/whakaara/activities/MainActivity.kt index 0dee3d8c..eccca03d 100644 --- a/app/src/main/java/com/app/whakaara/activities/MainActivity.kt +++ b/app/src/main/java/com/app/whakaara/activities/MainActivity.kt @@ -75,4 +75,14 @@ class MainActivity : ComponentActivity() { } } } + + override fun onResume() { + super.onResume() + viewModel.recreateTimer() + } + + override fun onPause() { + super.onPause() + viewModel.saveTimerStateForRecreation() + } } diff --git a/app/src/main/java/com/app/whakaara/data/datastore/PreferencesDataStore.kt b/app/src/main/java/com/app/whakaara/data/datastore/PreferencesDataStore.kt index 603f6836..87c6c10e 100644 --- a/app/src/main/java/com/app/whakaara/data/datastore/PreferencesDataStore.kt +++ b/app/src/main/java/com/app/whakaara/data/datastore/PreferencesDataStore.kt @@ -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 @@ -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) } @@ -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 } diff --git a/app/src/main/java/com/app/whakaara/logic/CountDownTimerUtil.kt b/app/src/main/java/com/app/whakaara/logic/CountDownTimerUtil.kt index ddd71cec..a3151f1d 100644 --- a/app/src/main/java/com/app/whakaara/logic/CountDownTimerUtil.kt +++ b/app/src/main/java/com/app/whakaara/logic/CountDownTimerUtil.kt @@ -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 @@ -26,6 +25,6 @@ class CountDownTimerUtil @Inject constructor() { } fun cancel() { - timer.cancel() + if (::timer.isInitialized) timer.cancel() } } diff --git a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt index 22a0523b..23c6418b 100644 --- a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt +++ b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt @@ -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 @@ -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 @@ -230,12 +234,66 @@ 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( + TimerStateDataStore( + remainingTimeInMillis = 0L, + isActive = false, + isPaused = false, + timeStamp = 0L + ) + ) + } + + 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() + ) + ) + } else { + preferencesDatastore.saveTimerData( + TimerStateDataStore( + remainingTimeInMillis = 0L, + isActive = false, + isPaused = false, + timeStamp = 0L + ) + ) + } + } // endregion } diff --git a/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt b/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt index 2089ec00..3ae13a80 100644 --- a/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt +++ b/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt @@ -8,9 +8,11 @@ import android.content.Context import android.content.Intent 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 @@ -32,7 +34,9 @@ import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_RECEIVE import com.app.whakaara.utils.constants.NotificationUtilsConstants.TIMER_RECEIVER_ACTION_STOP import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.runBlocking import java.util.Calendar +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Named @@ -42,7 +46,8 @@ class TimerManagerWrapper @Inject constructor( private val notificationManager: NotificationManager, @Named("timer") private val timerNotificationBuilder: NotificationCompat.Builder, - private val countDownTimerUtil: CountDownTimerUtil + private val countDownTimerUtil: CountDownTimerUtil, + private val preferencesDatastore: PreferencesDataStore ) { val timerState = MutableStateFlow(TimerState()) @@ -70,6 +75,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) { @@ -137,10 +160,36 @@ class TimerManagerWrapper @Inject constructor( millisecondsFromTimerInput = ZERO_MILLIS ) } + runBlocking { + preferencesDatastore.saveTimerData( + TimerStateDataStore( + remainingTimeInMillis = 0L, + isActive = false, + isPaused = false + ) + ) + } } ) } + 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() diff --git a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt index 8e900c95..37d30828 100644 --- a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt +++ b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt @@ -12,6 +12,7 @@ import android.content.Context import android.graphics.Color import androidx.core.app.NotificationCompat import com.app.whakaara.R +import com.app.whakaara.data.datastore.PreferencesDataStore import com.app.whakaara.logic.AlarmManagerWrapper import com.app.whakaara.logic.CountDownTimerUtil import com.app.whakaara.logic.StopwatchManagerWrapper @@ -146,8 +147,9 @@ class NotificationModule { notificationManager: NotificationManager, @Named("timer") timerNotificationBuilder: NotificationCompat.Builder, - countDownTimerUtil: CountDownTimerUtil - ): TimerManagerWrapper = TimerManagerWrapper(app, alarmManager, notificationManager, timerNotificationBuilder, countDownTimerUtil) + countDownTimerUtil: CountDownTimerUtil, + preferencesDataStore: PreferencesDataStore + ): TimerManagerWrapper = TimerManagerWrapper(app, alarmManager, notificationManager, timerNotificationBuilder, countDownTimerUtil, preferencesDataStore) @Provides @Singleton diff --git a/app/src/main/java/com/app/whakaara/state/TimerState.kt b/app/src/main/java/com/app/whakaara/state/TimerState.kt index a13abc1f..1386edfb 100644 --- a/app/src/main/java/com/app/whakaara/state/TimerState.kt +++ b/app/src/main/java/com/app/whakaara/state/TimerState.kt @@ -17,3 +17,10 @@ data class TimerState( val time: String = TIMER_STARTING_FORMAT, val millisecondsFromTimerInput: Long = ZERO_MILLIS ) + +data class TimerStateDataStore( + val remainingTimeInMillis: Long = 0L, + val isActive: Boolean = false, + val isPaused: Boolean = false, + val timeStamp: Long = 0L +) From 11ed0600651ff6aed1a0b7d871a3aaad9d16fc6b Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 11:19:59 +0100 Subject: [PATCH 2/7] fix test --- .../test/java/com/app/whakaara/logic/MainViewModelTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/com/app/whakaara/logic/MainViewModelTest.kt b/app/src/test/java/com/app/whakaara/logic/MainViewModelTest.kt index f4a95af9..a9cee255 100644 --- a/app/src/test/java/com/app/whakaara/logic/MainViewModelTest.kt +++ b/app/src/test/java/com/app/whakaara/logic/MainViewModelTest.kt @@ -4,6 +4,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test 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.data.preferences.SettingsTime @@ -52,6 +53,7 @@ class MainViewModelTest { private lateinit var stopwatchManagerWrapper: StopwatchManagerWrapper private lateinit var stopwatchState: StopwatchState private lateinit var timerState: TimerState + private lateinit var preferencesDataStore: PreferencesDataStore @Before fun setUp() { @@ -61,13 +63,14 @@ class MainViewModelTest { alarmManagerWrapper = mockk() timerManagerWrapper = mockk() stopwatchManagerWrapper = mockk() + preferencesDataStore = mockk() stopwatchState = StopwatchState() timerState = TimerState() every { stopwatchManagerWrapper.stopwatchState } returns MutableStateFlow(stopwatchState) every { timerManagerWrapper.timerState } returns MutableStateFlow(timerState) - viewModel = MainViewModel(repository, preferencesRepository, alarmManagerWrapper, timerManagerWrapper, stopwatchManagerWrapper) + viewModel = MainViewModel(repository, preferencesRepository, alarmManagerWrapper, timerManagerWrapper, stopwatchManagerWrapper, preferencesDataStore) alarms = listOf( Alarm( From 6533e4c5fece84825635bd682ce00ea816eca8a6 Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 13:37:20 +0100 Subject: [PATCH 3/7] remove calls on every pause/resume --- .../com/app/whakaara/logic/MainViewModel.kt | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt index 23c6418b..4c911e36 100644 --- a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt +++ b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt @@ -262,16 +262,16 @@ class MainViewModel @Inject constructor( milliseconds = status.remainingTimeInMillis - difference ) } - } - preferencesDatastore.saveTimerData( - TimerStateDataStore( - remainingTimeInMillis = 0L, - isActive = false, - isPaused = false, - timeStamp = 0L + preferencesDatastore.saveTimerData( + TimerStateDataStore( + remainingTimeInMillis = 0L, + isActive = false, + isPaused = false, + timeStamp = 0L + ) ) - ) + } } fun saveTimerStateForRecreation() = viewModelScope.launch(Dispatchers.IO) { @@ -284,15 +284,6 @@ class MainViewModel @Inject constructor( timeStamp = System.currentTimeMillis() ) ) - } else { - preferencesDatastore.saveTimerData( - TimerStateDataStore( - remainingTimeInMillis = 0L, - isActive = false, - isPaused = false, - timeStamp = 0L - ) - ) } } // endregion From 1327ad8768fa0e2df8b799a463bceaf5aa7055ad Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 19:01:36 +0100 Subject: [PATCH 4/7] setup module for coroutine scope injection, move hiltbroadcastreceiver + goAsync into separate files, reset timer datastore on reset timer function - prevent recreation when press stop on notification when app closed. --- .../com/app/whakaara/logic/MainViewModel.kt | 7 +-- .../app/whakaara/logic/TimerManagerWrapper.kt | 38 +++++++++++----- .../whakaara/module/CoroutinesScopesModule.kt | 21 +++++++++ .../app/whakaara/module/NotificationModule.kt | 6 ++- .../receiver/HiltBroadcastReceiver.kt | 11 +++++ .../whakaara/receiver/MediaServiceReceiver.kt | 45 +------------------ .../whakaara/receiver/ReceiverExtensions.kt | 45 +++++++++++++++++++ .../receiver/RecreateAlarmsReceiver.kt | 7 --- .../whakaara/receiver/StopwatchReceiver.kt | 4 +- .../app/whakaara/receiver/TimerReceiver.kt | 4 +- 10 files changed, 115 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt create mode 100644 app/src/main/java/com/app/whakaara/receiver/HiltBroadcastReceiver.kt create mode 100644 app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt diff --git a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt index 4c911e36..53e03cca 100644 --- a/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt +++ b/app/src/main/java/com/app/whakaara/logic/MainViewModel.kt @@ -264,12 +264,7 @@ class MainViewModel @Inject constructor( } preferencesDatastore.saveTimerData( - TimerStateDataStore( - remainingTimeInMillis = 0L, - isActive = false, - isPaused = false, - timeStamp = 0L - ) + state = TimerStateDataStore() ) } } diff --git a/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt b/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt index 3ae13a80..8ab8b384 100644 --- a/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt +++ b/app/src/main/java/com/app/whakaara/logic/TimerManagerWrapper.kt @@ -6,6 +6,7 @@ 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 @@ -32,13 +33,16 @@ 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.runBlocking +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, @@ -47,7 +51,8 @@ class TimerManagerWrapper @Inject constructor( @Named("timer") private val timerNotificationBuilder: NotificationCompat.Builder, private val countDownTimerUtil: CountDownTimerUtil, - private val preferencesDatastore: PreferencesDataStore + private val preferencesDatastore: PreferencesDataStore, + private val coroutineScope: CoroutineScope ) { val timerState = MutableStateFlow(TimerState()) @@ -160,15 +165,7 @@ class TimerManagerWrapper @Inject constructor( millisecondsFromTimerInput = ZERO_MILLIS ) } - runBlocking { - preferencesDatastore.saveTimerData( - TimerStateDataStore( - remainingTimeInMillis = 0L, - isActive = false, - isPaused = false - ) - ) - } + resetTimerStateDataStore() } ) } @@ -222,6 +219,7 @@ class TimerManagerWrapper @Inject constructor( millisecondsFromTimerInput = ZERO_MILLIS ) } + resetTimerStateDataStore() } fun restartTimer() { @@ -370,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 { diff --git a/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt new file mode 100644 index 00000000..9b84acf6 --- /dev/null +++ b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt @@ -0,0 +1,21 @@ +package com.app.whakaara.module + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +object CoroutinesScopesModule { + + @Singleton + @Provides + fun providesCoroutineScope(): CoroutineScope { + return CoroutineScope(SupervisorJob() + Dispatchers.IO) + } +} diff --git a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt index 37d30828..d1bd5980 100644 --- a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt +++ b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt @@ -24,6 +24,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope import javax.inject.Named import javax.inject.Singleton @@ -148,8 +149,9 @@ class NotificationModule { @Named("timer") timerNotificationBuilder: NotificationCompat.Builder, countDownTimerUtil: CountDownTimerUtil, - preferencesDataStore: PreferencesDataStore - ): TimerManagerWrapper = TimerManagerWrapper(app, alarmManager, notificationManager, timerNotificationBuilder, countDownTimerUtil, preferencesDataStore) + preferencesDataStore: PreferencesDataStore, + coroutineScope: CoroutineScope + ): TimerManagerWrapper = TimerManagerWrapper(app, alarmManager, notificationManager, timerNotificationBuilder, countDownTimerUtil, preferencesDataStore, coroutineScope) @Provides @Singleton diff --git a/app/src/main/java/com/app/whakaara/receiver/HiltBroadcastReceiver.kt b/app/src/main/java/com/app/whakaara/receiver/HiltBroadcastReceiver.kt new file mode 100644 index 00000000..56343fc7 --- /dev/null +++ b/app/src/main/java/com/app/whakaara/receiver/HiltBroadcastReceiver.kt @@ -0,0 +1,11 @@ +package com.app.whakaara.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import androidx.annotation.CallSuper + +abstract class HiltBroadcastReceiver : BroadcastReceiver() { + @CallSuper + override fun onReceive(context: Context, intent: Intent) {} +} diff --git a/app/src/main/java/com/app/whakaara/receiver/MediaServiceReceiver.kt b/app/src/main/java/com/app/whakaara/receiver/MediaServiceReceiver.kt index dab48211..ab611062 100644 --- a/app/src/main/java/com/app/whakaara/receiver/MediaServiceReceiver.kt +++ b/app/src/main/java/com/app/whakaara/receiver/MediaServiceReceiver.kt @@ -1,7 +1,6 @@ package com.app.whakaara.receiver import android.app.NotificationManager -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.util.Log @@ -14,17 +13,10 @@ import com.app.whakaara.utils.constants.NotificationUtilsConstants.NOTIFICATION_ import com.app.whakaara.utils.constants.NotificationUtilsConstants.NOTIFICATION_TYPE_ALARM import com.app.whakaara.utils.constants.NotificationUtilsConstants.NOTIFICATION_TYPE_TIMER import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.cancellation.CancellationException @AndroidEntryPoint -class MediaServiceReceiver : BroadcastReceiver() { +class MediaServiceReceiver : HiltBroadcastReceiver() { @Inject lateinit var notificationManager: NotificationManager @@ -36,6 +28,7 @@ class MediaServiceReceiver : BroadcastReceiver() { lateinit var timerManagerWrapper: TimerManagerWrapper override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) val alarmId = intent.getStringExtra(INTENT_ALARM_ID) ?: "" val alarmType = intent.getIntExtra(NOTIFICATION_TYPE, -1) if (alarmType == -1) { @@ -56,37 +49,3 @@ class MediaServiceReceiver : BroadcastReceiver() { } } } - -// https://github.com/androidx/androidx/blob/a00488668925d695a6ae0d6168d33fdd619c0b31/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CoroutineBroadcastReceiver.kt#L35 -fun BroadcastReceiver.goAsync( - coroutineContext: CoroutineContext = Dispatchers.Default, - block: suspend CoroutineScope.() -> Unit -) { - val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext) - val pendingResult = goAsync() - - coroutineScope.launch { - try { - try { - block() - } catch (e: CancellationException) { - throw e - } catch (t: Throwable) { - Log.e("goAsyncTAG", "BroadcastReceiver execution failed", t) - } finally { - // Nothing can be in the `finally` block after this, as this throws a - // `CancellationException` - coroutineScope.cancel() - } - } finally { - // This must be the last call, as the process may be killed after calling this. - try { - pendingResult.finish() - } catch (e: IllegalStateException) { - // On some OEM devices, this may throw an error about "Broadcast already finished". - // See b/257513022. - Log.e("goAsyncTAG", "Error thrown when trying to finish broadcast", e) - } - } - } -} diff --git a/app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt b/app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt new file mode 100644 index 00000000..d1f69b60 --- /dev/null +++ b/app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt @@ -0,0 +1,45 @@ +package com.app.whakaara.receiver + +import android.content.BroadcastReceiver +import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.cancellation.CancellationException + +// https://github.com/androidx/androidx/blob/a00488668925d695a6ae0d6168d33fdd619c0b31/glance/glance-appwidget/src/main/java/androidx/glance/appwidget/CoroutineBroadcastReceiver.kt#L35 +fun BroadcastReceiver.goAsync( + coroutineContext: CoroutineContext = Dispatchers.Default, + block: suspend CoroutineScope.() -> Unit +) { + val coroutineScope = CoroutineScope(SupervisorJob() + coroutineContext) + val pendingResult = goAsync() + + coroutineScope.launch { + try { + try { + block() + } catch (e: CancellationException) { + throw e + } catch (t: Throwable) { + Log.e("goAsyncTAG", "BroadcastReceiver execution failed", t) + } finally { + // Nothing can be in the `finally` block after this, as this throws a + // `CancellationException` + coroutineScope.cancel() + } + } finally { + // This must be the last call, as the process may be killed after calling this. + try { + pendingResult.finish() + } catch (e: IllegalStateException) { + // On some OEM devices, this may throw an error about "Broadcast already finished". + // See b/257513022. + Log.e("goAsyncTAG", "Error thrown when trying to finish broadcast", e) + } + } + } +} diff --git a/app/src/main/java/com/app/whakaara/receiver/RecreateAlarmsReceiver.kt b/app/src/main/java/com/app/whakaara/receiver/RecreateAlarmsReceiver.kt index d8618460..c968e9e9 100644 --- a/app/src/main/java/com/app/whakaara/receiver/RecreateAlarmsReceiver.kt +++ b/app/src/main/java/com/app/whakaara/receiver/RecreateAlarmsReceiver.kt @@ -1,9 +1,7 @@ package com.app.whakaara.receiver -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import androidx.annotation.CallSuper import com.app.whakaara.data.alarm.AlarmRepository import com.app.whakaara.data.preferences.PreferencesRepository import com.app.whakaara.logic.AlarmManagerWrapper @@ -52,8 +50,3 @@ class RecreateAlarmsReceiver : HiltBroadcastReceiver() { } } } - -abstract class HiltBroadcastReceiver : BroadcastReceiver() { - @CallSuper - override fun onReceive(context: Context, intent: Intent) {} -} diff --git a/app/src/main/java/com/app/whakaara/receiver/StopwatchReceiver.kt b/app/src/main/java/com/app/whakaara/receiver/StopwatchReceiver.kt index 51d4ef88..c0eddece 100644 --- a/app/src/main/java/com/app/whakaara/receiver/StopwatchReceiver.kt +++ b/app/src/main/java/com/app/whakaara/receiver/StopwatchReceiver.kt @@ -1,6 +1,5 @@ package com.app.whakaara.receiver -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.app.whakaara.logic.StopwatchManagerWrapper @@ -11,11 +10,12 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class StopwatchReceiver : BroadcastReceiver() { +class StopwatchReceiver : HiltBroadcastReceiver() { @Inject lateinit var stopwatchManagerWrapper: StopwatchManagerWrapper override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) val actionsList = listOf(STOPWATCH_RECEIVER_ACTION_START, STOPWATCH_RECEIVER_ACTION_PAUSE, STOPWATCH_RECEIVER_ACTION_STOP) if (!actionsList.contains(intent.action)) return diff --git a/app/src/main/java/com/app/whakaara/receiver/TimerReceiver.kt b/app/src/main/java/com/app/whakaara/receiver/TimerReceiver.kt index 6e71da2d..13090e5f 100644 --- a/app/src/main/java/com/app/whakaara/receiver/TimerReceiver.kt +++ b/app/src/main/java/com/app/whakaara/receiver/TimerReceiver.kt @@ -1,6 +1,5 @@ package com.app.whakaara.receiver -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import com.app.whakaara.logic.TimerManagerWrapper @@ -11,11 +10,12 @@ import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint -class TimerReceiver : BroadcastReceiver() { +class TimerReceiver : HiltBroadcastReceiver() { @Inject lateinit var timerManagerWrapper: TimerManagerWrapper override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) val actionsList = listOf(TIMER_RECEIVER_ACTION_PAUSE, TIMER_RECEIVER_ACTION_STOP, TIMER_RECEIVER_ACTION_START) if (!actionsList.contains(intent.action)) return From 1b0ccc7f31dad0790d2998645d45238aaa17cfc1 Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 19:09:12 +0100 Subject: [PATCH 5/7] rename file for linting --- .../app/whakaara/receiver/{ReceiverExtensions.kt => GoAsync.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/app/whakaara/receiver/{ReceiverExtensions.kt => GoAsync.kt} (100%) diff --git a/app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt b/app/src/main/java/com/app/whakaara/receiver/GoAsync.kt similarity index 100% rename from app/src/main/java/com/app/whakaara/receiver/ReceiverExtensions.kt rename to app/src/main/java/com/app/whakaara/receiver/GoAsync.kt From b8586b64537aec41cedd9cab1eec4d90be842696 Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Mon, 15 Apr 2024 23:47:21 +0100 Subject: [PATCH 6/7] inject coroutine scope --- .../whakaara/logic/StopwatchManagerWrapper.kt | 10 ++-- .../module/CoroutinesDispatchersModule.kt | 46 +++++++++++++++++++ .../whakaara/module/CoroutinesScopesModule.kt | 8 ++-- .../app/whakaara/module/NotificationModule.kt | 5 +- 4 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/app/whakaara/module/CoroutinesDispatchersModule.kt diff --git a/app/src/main/java/com/app/whakaara/logic/StopwatchManagerWrapper.kt b/app/src/main/java/com/app/whakaara/logic/StopwatchManagerWrapper.kt index 8317a360..2a91035d 100644 --- a/app/src/main/java/com/app/whakaara/logic/StopwatchManagerWrapper.kt +++ b/app/src/main/java/com/app/whakaara/logic/StopwatchManagerWrapper.kt @@ -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 @@ -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() { @@ -88,8 +87,7 @@ class StopwatchManagerWrapper @Inject constructor( } fun resetStopwatch() { - coroutineScope.cancel() - coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope.coroutineContext.cancelChildren() cancelNotification() stopwatchState.update { it.copy( diff --git a/app/src/main/java/com/app/whakaara/module/CoroutinesDispatchersModule.kt b/app/src/main/java/com/app/whakaara/module/CoroutinesDispatchersModule.kt new file mode 100644 index 00000000..25d7bee9 --- /dev/null +++ b/app/src/main/java/com/app/whakaara/module/CoroutinesDispatchersModule.kt @@ -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 +} diff --git a/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt index 9b84acf6..e6ef8828 100644 --- a/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt +++ b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt @@ -4,8 +4,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @@ -15,7 +15,9 @@ object CoroutinesScopesModule { @Singleton @Provides - fun providesCoroutineScope(): CoroutineScope { - return CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun providesCoroutineScope( + @DefaultDispatcher defaultDispatcher: CoroutineDispatcher + ): CoroutineScope { + return CoroutineScope(SupervisorJob() + defaultDispatcher) } } diff --git a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt index d1bd5980..cb185a5c 100644 --- a/app/src/main/java/com/app/whakaara/module/NotificationModule.kt +++ b/app/src/main/java/com/app/whakaara/module/NotificationModule.kt @@ -159,8 +159,9 @@ class NotificationModule { app: Application, notificationManager: NotificationManager, @Named("stopwatch") - stopwatchNotificationBuilder: NotificationCompat.Builder - ): StopwatchManagerWrapper = StopwatchManagerWrapper(app, notificationManager, stopwatchNotificationBuilder) + stopwatchNotificationBuilder: NotificationCompat.Builder, + coroutineScope: CoroutineScope + ): StopwatchManagerWrapper = StopwatchManagerWrapper(app, notificationManager, stopwatchNotificationBuilder, coroutineScope) @Provides @Singleton From 9a5fa0fb3d1f17198338b8394939070c9fe8f0d5 Mon Sep 17 00:00:00 2001 From: Anaru Hudson Date: Tue, 16 Apr 2024 10:54:03 +0100 Subject: [PATCH 7/7] remove singleton scope --- .../main/java/com/app/whakaara/module/CoroutinesScopesModule.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt index e6ef8828..5c6d87f4 100644 --- a/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt +++ b/app/src/main/java/com/app/whakaara/module/CoroutinesScopesModule.kt @@ -7,13 +7,11 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob -import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module object CoroutinesScopesModule { - @Singleton @Provides fun providesCoroutineScope( @DefaultDispatcher defaultDispatcher: CoroutineDispatcher