diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
new file mode 100644
index 0000000..7752686
--- /dev/null
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 93752fd..0007cff 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -139,6 +139,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.androidx.adaptive.navigation3)
implementation(libs.androidx.appcompat)
+ implementation(libs.kotlinx.datetime)
implementation(libs.lyricist)
ksp(libs.lyricist.processor)
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/framework/app/di/AppModule.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/framework/app/di/AppModule.kt
index f983703..449fad1 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/framework/app/di/AppModule.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/framework/app/di/AppModule.kt
@@ -28,8 +28,8 @@ import com.timilehinaregbesola.mathalarm.notification.AlarmNotificationScheduler
import com.timilehinaregbesola.mathalarm.notification.MathAlarmNotification
import com.timilehinaregbesola.mathalarm.notification.MathAlarmNotificationChannel
import com.timilehinaregbesola.mathalarm.presentation.appsettings.AppThemeOptionsMapper
-import com.timilehinaregbesola.mathalarm.provider.CalendarProvider
-import com.timilehinaregbesola.mathalarm.provider.CalendarProviderImpl
+import com.timilehinaregbesola.mathalarm.provider.DateTimeProvider
+import com.timilehinaregbesola.mathalarm.provider.DateTimeProviderImpl
import com.timilehinaregbesola.mathalarm.usecases.AddAlarm
import com.timilehinaregbesola.mathalarm.usecases.CancelAlarm
import com.timilehinaregbesola.mathalarm.usecases.ClearAlarms
@@ -89,8 +89,8 @@ object AppModule {
@Provides
@Singleton
- fun provideCalenderProvider(): CalendarProvider {
- return CalendarProviderImpl()
+ fun provideCalenderProvider(): DateTimeProvider {
+ return DateTimeProviderImpl()
}
@Provides
@@ -123,7 +123,7 @@ object AppModule {
repository: AlarmRepository,
alarmInteractor: AlarmInteractor,
notificationInteractor: NotificationInteractor,
- calendarProvider: CalendarProvider,
+ calendarProvider: DateTimeProvider,
scheduleNextAlarm: ScheduleNextAlarm
): Usecases {
return Usecases(
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/interactors/AlarmInteractorImpl.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/interactors/AlarmInteractorImpl.kt
index 193c7c6..cf3b3a3 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/interactors/AlarmInteractorImpl.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/interactors/AlarmInteractorImpl.kt
@@ -8,12 +8,15 @@ class AlarmInteractorImpl(private val alarmManager: AlarmNotificationScheduler)
AlarmInteractor {
override fun schedule(alarm: Alarm, reschedule: Boolean): Boolean {
- Timber.d("schedule - alarmId = ${alarm.alarmId}")
- return alarmManager.scheduleAlarm(alarm, reschedule)
+ Timber.d("AlarmInteractorImpl.schedule called: alarmId=${alarm.alarmId}, time=${alarm.hour}:${alarm.minute}, repeat=${alarm.repeat}, repeatDays=${alarm.repeatDays}, reschedule=$reschedule")
+ val result = alarmManager.scheduleAlarm(alarm, reschedule)
+ Timber.d("AlarmInteractorImpl.schedule result for alarmId=${alarm.alarmId}: $result")
+ return result
}
override fun cancel(alarm: Alarm) {
- Timber.d("cancel - alarmId = ${alarm.alarmId}")
+ Timber.d("AlarmInteractorImpl.cancel called: alarmId=${alarm.alarmId}, time=${alarm.hour}:${alarm.minute}, repeat=${alarm.repeat}, repeatDays=${alarm.repeatDays}")
alarmManager.cancelAlarm(alarm)
+ Timber.d("AlarmInteractorImpl.cancel completed for alarmId=${alarm.alarmId}")
}
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationScheduler.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationScheduler.kt
index 51ff72d..08c705b 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationScheduler.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationScheduler.kt
@@ -11,9 +11,25 @@ import com.timilehinaregbesola.mathalarm.AlarmReceiver
import com.timilehinaregbesola.mathalarm.AlarmReceiver.Companion.ALARM_ACTION
import com.timilehinaregbesola.mathalarm.AlarmReceiver.Companion.EXTRA_TASK
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
-import com.timilehinaregbesola.mathalarm.utils.*
+import com.timilehinaregbesola.mathalarm.utils.SAT
+import com.timilehinaregbesola.mathalarm.utils.SUN
+import com.timilehinaregbesola.mathalarm.utils.cancelAlarm
+import com.timilehinaregbesola.mathalarm.utils.fullDays
+import com.timilehinaregbesola.mathalarm.utils.initLocalDateTimeInSystemZone
+import com.timilehinaregbesola.mathalarm.utils.setExactAlarm
+import com.timilehinaregbesola.mathalarm.utils.toIndex
+import kotlinx.datetime.DatePeriod
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.plus
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
import timber.log.Timber
-import java.util.*
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
/**
* Alarm manager to schedule an event based on the time from a Alarm.
@@ -26,23 +42,35 @@ class AlarmNotificationScheduler(private val context: Context) {
* @param passedAlarm alarm to be scheduled
* @param reschedule whether alarm is repeating
*/
+ @OptIn(ExperimentalTime::class)
@SuppressLint("UnspecifiedImmutableFlag")
fun scheduleAlarm(passedAlarm: Alarm, reschedule: Boolean): Boolean {
- Timber.d("Schedule alarm..")
+ Timber.d("Schedule alarm for id=${passedAlarm.alarmId}, time=${passedAlarm.hour}:${passedAlarm.minute}, repeat=${passedAlarm.repeat}, repeatDays=${passedAlarm.repeatDays}, reschedule=$reschedule")
val alarmIntent = Intent(context, AlarmReceiver::class.java).apply {
action = ALARM_ACTION
putExtra(EXTRA_TASK, passedAlarm.alarmId)
}
val alarmIntentList: MutableList = ArrayList()
- val time: MutableList = ArrayList()
+ val timeInstants: MutableList = ArrayList()
+ val tz = TimeZone.currentSystemDefault()
+ var hasExistingAlarms = false
+
// If there is no day set, set the alarm on the closest possible date
if (passedAlarm.repeatDays == "FFFFFFF") {
- val cal = passedAlarm.initCalendar()
- var dayOfTheWeek = getDayOfWeek(cal[Calendar.DAY_OF_WEEK])
- if (cal.timeInMillis > System.currentTimeMillis()) { // set it today
+ Timber.d("No repeat days set, determining closest possible date")
+ val dateTime = passedAlarm.initLocalDateTimeInSystemZone()
+ val instant = dateTime.toInstant(tz)
+ val nowInstant = Clock.System.now()
+ Timber.d("Alarm datetime: $dateTime, instant: $instant, now: $nowInstant")
+
+ var dayOfTheWeek = dateTime.date.dayOfWeek.toIndex()
+ Timber.d("Current day of week: $dayOfTheWeek")
+
+ if (instant > nowInstant) { // set it today
val sb = StringBuilder("FFFFFFF")
sb.setCharAt(dayOfTheWeek, 'T')
passedAlarm.repeatDays = sb.toString()
+ Timber.d("Alarm time is in the future, setting for today. New repeatDays: ${passedAlarm.repeatDays}")
} else { // alarm time already passed for the day so set it tomorrow
val sb = StringBuilder("FFFFFFF")
if (dayOfTheWeek == SAT) { // if it is saturday
@@ -52,35 +80,62 @@ class AlarmNotificationScheduler(private val context: Context) {
}
sb.setCharAt(dayOfTheWeek, 'T')
passedAlarm.repeatDays = sb.toString()
+ Timber.d("Alarm time already passed, setting for tomorrow (day $dayOfTheWeek). New repeatDays: ${passedAlarm.repeatDays}")
}
}
+
for (i in SUN..SAT) {
if (passedAlarm.repeatDays[i] == 'T') {
+ Timber.d("Processing day $i (${fullDays[i]}) which is set to true")
+ val nowInstant = Clock.System.now()
+ val localNow = nowInstant.toLocalDateTime(tz)
+ val todayDate = localNow.date
+
+ val currentDay = todayDate.dayOfWeek.toIndex()
+
+ Timber.d("Current day: $currentDay (${fullDays[currentDay]})")
+
val daysUntilAlarm: Int
- val cal = passedAlarm.initCalendar()
- val currentDay = getDayOfWeek(cal[Calendar.DAY_OF_WEEK])
- Timber.d("current day: $currentDay")
- if (currentDay > i ||
- (currentDay == i && cal.timeInMillis < System.currentTimeMillis())
- ) {
+ val targetDate: LocalDate
+
+ val alarmTimeToday = passedAlarm.initLocalDateTimeInSystemZone()
+ val alarmInstantToday = alarmTimeToday.toInstant(tz)
+ Timber.d("Alarm time today would be: $alarmTimeToday (${alarmInstantToday})")
+ Timber.d("Current time is: $localNow (${nowInstant})")
+
+ val isPastToday = alarmInstantToday < nowInstant
+ Timber.d("Is alarm time past for today? $isPastToday")
+
+ if (currentDay > i || (currentDay == i && isPastToday)) {
// days left till end of week(sat) + the day of the week of the alarm
// EX: alarm = i = tues = 2; current = wed = 3; end of week = sat = 6
// end - current = 6 - 3 = 3 -> 3 days till saturday/end of week
// end of week + 1 (to sunday) + day of week alarm is on = 3 + 1 + 2 = 6
daysUntilAlarm = SAT - currentDay + 1 + i
- cal.add(Calendar.DAY_OF_YEAR, daysUntilAlarm)
- Timber.d("days until alarm: $daysUntilAlarm")
+ targetDate = todayDate.plus(DatePeriod(days = daysUntilAlarm))
+ Timber.d("Current day ($currentDay) > alarm day ($i) or same day but time passed, scheduling for next week")
+ Timber.d("Days until alarm: $daysUntilAlarm, target date: $targetDate")
} else {
daysUntilAlarm = i - currentDay
- cal.add(Calendar.DAY_OF_YEAR, daysUntilAlarm)
- Timber.d("days until alarm: $daysUntilAlarm")
+ targetDate = todayDate.plus(DatePeriod(days = daysUntilAlarm))
+ Timber.d("Current day ($currentDay) <= alarm day ($i) and time not passed, scheduling for this week")
+ Timber.d("Days until alarm: $daysUntilAlarm, target date: $targetDate")
}
+
+ val targetDateTime = LocalDateTime(
+ date = targetDate,
+ time = LocalTime(passedAlarm.hour, passedAlarm.minute, 0)
+ )
+ val targetInstant = targetDateTime.toInstant(tz)
+
val stringId: StringBuilder = StringBuilder().append(passedAlarm.alarmId).append(i)
.append(passedAlarm.hour).append(passedAlarm.minute)
val id = stringId.toString().split("-").joinToString("")
val intentId = id.toInt()
- Timber.d("intent id: $intentId")
- // check if a previous alarm has been set
+ Timber.d("Generated intent ID: $intentId for alarm ID: ${passedAlarm.alarmId}, day: $i, time: ${passedAlarm.hour}:${passedAlarm.minute}")
+
+ // Check if a previous alarm has been set
+ Timber.d("Checking if a previous alarm with this ID already exists")
val isSet = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
@@ -91,37 +146,63 @@ class AlarmNotificationScheduler(private val context: Context) {
} else {
PendingIntent.getBroadcast(context, intentId, alarmIntent, PendingIntent.FLAG_NO_CREATE)
}
+
if (isSet != null) {
+ Timber.d("An alarm with ID $intentId already exists")
+ hasExistingAlarms = true
if (!reschedule) {
+ Timber.d("Not rescheduling because reschedule flag is false")
// context.showToast(R.string.alarm_duplicate_toast_text)
+ } else {
+ // If reschedule is true, cancel the existing alarm and create a new one
+ Timber.d("Canceling existing alarm because reschedule flag is true")
+ context.cancelAlarm(isSet)
+ isSet.cancel()
}
- return false
}
- val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- PendingIntent.getBroadcast(
- context,
- intentId,
- alarmIntent,
- PendingIntent.FLAG_CANCEL_CURRENT or FLAG_MUTABLE,
- )
- } else {
- PendingIntent.getBroadcast(
- context,
- intentId,
- alarmIntent,
- PendingIntent.FLAG_CANCEL_CURRENT,
- )
+
+ // If reschedule is true or no existing alarm was found, create a new one
+ if (isSet == null || reschedule) {
+ Timber.d("Proceeding to create new alarm")
+
+ val pendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ PendingIntent.getBroadcast(
+ context,
+ intentId,
+ alarmIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or FLAG_MUTABLE,
+ )
+ } else {
+ PendingIntent.getBroadcast(
+ context,
+ intentId,
+ alarmIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT,
+ )
+ }
+
+ alarmIntentList.add(pendingIntent)
+ timeInstants.add(targetInstant)
}
- alarmIntentList.add(pendingIntent)
- time.add(cal)
}
}
+
+ // Return true if we scheduled new alarms OR if there were existing alarms
+ if (alarmIntentList.isEmpty() && !hasExistingAlarms) {
+ Timber.w("No alarms were scheduled and no existing alarms found")
+ return false
+ }
+
+ Timber.d("Scheduling ${alarmIntentList.size} alarms")
for (i in alarmIntentList.indices) {
val pendingIntent = alarmIntentList[i]
- val cal = time[i]
- context.setExactAlarm(cal.timeInMillis, pendingIntent)
- Timber.d("scheduled new alarm")
+ val instant = timeInstants[i]
+ Timber.d("Scheduling alarm #${i+1}/${alarmIntentList.size} for time: ${instant}")
+ context.setExactAlarm(instant.toEpochMilliseconds(), pendingIntent)
+ Timber.d("Alarm #${i+1} scheduled successfully")
}
+
+ Timber.d("All ${alarmIntentList.size} alarms scheduled successfully, returning true")
return true
}
@@ -131,15 +212,23 @@ class AlarmNotificationScheduler(private val context: Context) {
* @param alarm alarm to be canceled
*/
fun cancelAlarm(alarm: Alarm) {
+ Timber.d("AlarmNotificationScheduler.cancelAlarm called: alarmId=${alarm.alarmId}, time=${alarm.hour}:${alarm.minute}, repeat=${alarm.repeat}, repeatDays=${alarm.repeatDays}")
+
val receiverIntent = Intent(context, AlarmReceiver::class.java)
receiverIntent.action = ALARM_ACTION
receiverIntent.putExtra(EXTRA_TASK, alarm.alarmId)
+
+ var canceledCount = 0
for (i in 0..6) { // For each day of the week
- if (alarm.repeatDays[i] == 'T') {
+ if (alarm.repeatDays.getOrNull(i) == 'T') {
+ Timber.d("Canceling alarm for day $i (${fullDays[i]})")
+
val stringId: StringBuilder = StringBuilder().append(alarm.alarmId).append(i)
.append(alarm.hour).append(alarm.minute)
val id = stringId.toString().split("-").joinToString("")
val intentId = id.toInt()
+ Timber.d("Generated intent ID: $intentId for alarm ID: ${alarm.alarmId}, day: $i, time: ${alarm.hour}:${alarm.minute}")
+
val cancelPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
@@ -155,9 +244,15 @@ class AlarmNotificationScheduler(private val context: Context) {
FLAG_UPDATE_CURRENT,
)
}
+
+ Timber.d("Calling context.cancelAlarm for intent ID: $intentId")
context.cancelAlarm(cancelPendingIntent)
cancelPendingIntent.cancel()
+ Timber.d("Alarm canceled for day $i (${fullDays[i]})")
+ canceledCount++
}
}
+
+ Timber.d("AlarmNotificationScheduler.cancelAlarm completed for alarmId=${alarm.alarmId}, canceled $canceledCount alarms")
}
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListEvent.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListEvent.kt
index 797d06c..3737dd2 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListEvent.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListEvent.kt
@@ -9,5 +9,6 @@ sealed class AlarmListEvent {
data class OnEditAlarmClick(val alarm: Alarm) : AlarmListEvent()
object OnAddAlarmClick : AlarmListEvent()
object OnClearAlarmsClick : AlarmListEvent()
+ object OnClearEmptyAlarmsClick : AlarmListEvent()
data class DeleteTestAlarm(val alarmId: Long) : AlarmListEvent()
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListViewModel.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListViewModel.kt
index b5d3e7b..35f8b4f 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListViewModel.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/AlarmListViewModel.kt
@@ -1,13 +1,13 @@
package com.timilehinaregbesola.mathalarm.presentation.alarmlist
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.framework.Usecases
import com.timilehinaregbesola.mathalarm.framework.app.permission.AlarmPermission
-import com.timilehinaregbesola.mathalarm.provider.CalendarProvider
import com.timilehinaregbesola.mathalarm.utils.UiEvent
+import com.timilehinaregbesola.mathalarm.utils.UiEvent.Navigate
+import com.timilehinaregbesola.mathalarm.utils.UiEvent.ShowSnackbar
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow
@@ -19,9 +19,7 @@ import javax.inject.Inject
@HiltViewModel
class AlarmListViewModel @Inject constructor(
private val usecases: Usecases,
- val calender: CalendarProvider,
val permission: AlarmPermission,
- val savedStateHandle: SavedStateHandle
) : ViewModel() {
var alarms = usecases.getSavedAlarms()
@@ -35,21 +33,25 @@ class AlarmListViewModel @Inject constructor(
when (event) {
is AlarmListEvent.OnEditAlarmClick -> {
// Navigate to bottom sheet
- sendUiEvent(UiEvent.Navigate(event.alarm))
+ sendUiEvent(Navigate(event.alarm))
}
is AlarmListEvent.OnAlarmOnChange -> {
+ Timber.d("OnAlarmOnChange event received: alarmId=${event.alarm.alarmId}, isOn=${event.isOn}")
viewModelScope.launch {
+ Timber.d("Updating alarm in database: alarmId=${event.alarm.alarmId}, setting isOn=${event.isOn}")
usecases.addAlarm(event.alarm.copy(isOn = event.isOn))
if (event.isOn) {
+ Timber.d("Alarm is being turned ON, scheduling: alarmId=${event.alarm.alarmId}")
usecases.scheduleAlarm(event.alarm, false)
} else {
+ Timber.d("Alarm is being turned OFF: alarmId=${event.alarm.alarmId}")
// Cancel
}
}
}
is AlarmListEvent.OnAddAlarmClick -> {
// Navigate to bottom sheet
- sendUiEvent(UiEvent.Navigate(Alarm()))
+ sendUiEvent(Navigate(Alarm()))
}
is AlarmListEvent.OnUndoDeleteClick -> {
viewModelScope.launch {
@@ -61,7 +63,7 @@ class AlarmListViewModel @Inject constructor(
viewModelScope.launch {
usecases.deleteAlarm(event.alarm)
recentlyDeletedAlarm = event.alarm
- sendUiEvent(UiEvent.ShowSnackbar(message = "Alarm Deleted", action = "Undo"))
+ sendUiEvent(ShowSnackbar(message = "Alarm Deleted", action = "Undo"))
}
}
is AlarmListEvent.DeleteTestAlarm -> {
@@ -79,6 +81,10 @@ class AlarmListViewModel @Inject constructor(
}
}
}
+
+ AlarmListEvent.OnClearEmptyAlarmsClick -> {
+ sendUiEvent(ShowSnackbar(message = "There are no alarms to clear"))
+ }
}
}
@@ -95,15 +101,21 @@ class AlarmListViewModel @Inject constructor(
}
fun scheduleAlarm(alarm: Alarm, reschedule: Boolean, message: String) {
+ Timber.d("scheduleAlarm called: alarmId=${alarm.alarmId}, time=${alarm.hour}:${alarm.minute}, repeat=${alarm.repeat}, repeatDays=${alarm.repeatDays}, reschedule=$reschedule")
viewModelScope.launch {
+ Timber.d("Calling usecases.scheduleAlarm for alarmId=${alarm.alarmId}")
usecases.scheduleAlarm(alarm, reschedule)
- sendUiEvent(UiEvent.ShowSnackbar(message = message))
+ Timber.d("scheduleAlarm completed for alarmId=${alarm.alarmId}, showing snackbar message: $message")
+ sendUiEvent(ShowSnackbar(message = message))
}
}
fun cancelAlarm(alarm: Alarm) {
+ Timber.d("cancelAlarm called: alarmId=${alarm.alarmId}, time=${alarm.hour}:${alarm.minute}, repeat=${alarm.repeat}, repeatDays=${alarm.repeatDays}")
viewModelScope.launch {
+ Timber.d("Calling usecases.cancelAlarm for alarmId=${alarm.alarmId}")
usecases.cancelAlarm(alarm)
+ Timber.d("cancelAlarm completed for alarmId=${alarm.alarmId}")
}
}
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListHeader.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListHeader.kt
index 77bdae6..079d7b5 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListHeader.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListHeader.kt
@@ -17,36 +17,34 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cafe.adriel.lyricist.strings
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
-import com.timilehinaregbesola.mathalarm.presentation.alarmlist.components.AlarmListHeader.LIST_HEADER_ALPHA
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.components.AlarmListHeader.LIST_HEADER_ELEVATION
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.components.AlarmListHeader.LIST_HEADER_FONT_SIZE
-import com.timilehinaregbesola.mathalarm.presentation.alarmlist.components.AlarmListHeader.ONE_WEEK_IN_MILLISECONDS
+import com.timilehinaregbesola.mathalarm.presentation.alarmlist.components.AlarmListHeader.ListHeaderAlpha
import com.timilehinaregbesola.mathalarm.presentation.ui.darkPrimaryLight
import com.timilehinaregbesola.mathalarm.presentation.ui.spacing
-import com.timilehinaregbesola.mathalarm.utils.getCalendarFromAlarm
+import com.timilehinaregbesola.mathalarm.utils.calculateNextAlarmTime
import com.timilehinaregbesola.mathalarm.utils.getTimeLeft
-import timber.log.Timber
-import java.util.Calendar
+import kotlinx.datetime.TimeZone
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
+@OptIn(ExperimentalTime::class)
@Composable
fun ListHeader(
modifier: Modifier = Modifier,
enabled: Boolean,
- calendar: Calendar,
+ timeZone: TimeZone = TimeZone.currentSystemDefault(),
alarmList: List,
isDark: Boolean
) {
val (nearestTime, nearestIndex) = buildNearestTime(
alarmList = alarmList,
- calendar = calendar
+ timeZone = timeZone
)
val nearestAlarmMessage by remember(nearestTime, nearestIndex) {
derivedStateOf {
- nearestTime?.let { time ->
- alarmList[nearestIndex].getTimeLeft(
- time,
- calendar
- )
+ nearestTime?.let {
+ alarmList.getOrNull(nearestIndex)?.getTimeLeft()
}
}
}
@@ -55,7 +53,7 @@ fun ListHeader(
.padding(top = MaterialTheme.spacing.small)
.fillMaxWidth()
.background(
- color = if (isDark) darkPrimaryLight else LightGray.copy(alpha = LIST_HEADER_ALPHA),
+ color = if (isDark) darkPrimaryLight else LightGray.copy(alpha = ListHeaderAlpha),
)
.then(modifier),
tonalElevation = LIST_HEADER_ELEVATION,
@@ -79,28 +77,25 @@ fun ListHeader(
}
}
+@OptIn(ExperimentalTime::class)
private fun buildNearestTime(
alarmList: List,
- calendar: Calendar
-): Pair {
- var nearestTime: Long? = null
+ timeZone: TimeZone
+): Pair {
+ var nearestTime: Instant? = null
var nearestIndex = -1
+
if (alarmList.isNotEmpty()) {
alarmList
- .filter { it.isOn }
- .forEachIndexed { index, alarm ->
- val cal = getCalendarFromAlarm(alarm, calendar)
- var alarmTime = cal.timeInMillis
+ .forEachIndexed { originalIndex, alarm ->
+ if (alarm.isOn) {
+ val alarmInstant = calculateNextAlarmTime(alarm, timeZone)
- val now = System.currentTimeMillis()
- Timber.d("time = $alarmTime")
- if (alarmTime < now) {
- alarmTime += ONE_WEEK_IN_MILLISECONDS
- }
- val timeToAlarm = alarmTime - now
- if (nearestTime == null || timeToAlarm < nearestTime!!) {
- nearestTime = timeToAlarm
- nearestIndex = index
+ // If a valid future time was found and it's sooner than the current nearest, update
+ if (alarmInstant != null && (nearestTime == null || alarmInstant < nearestTime)) {
+ nearestTime = alarmInstant
+ nearestIndex = originalIndex
+ }
}
}
}
@@ -113,7 +108,6 @@ private fun ListHeaderPreview() {
MaterialTheme {
ListHeader(
enabled = false,
- calendar = Calendar.getInstance(),
alarmList = emptyList(),
isDark = true
)
@@ -121,7 +115,7 @@ private fun ListHeaderPreview() {
}
private object AlarmListHeader {
- const val LIST_HEADER_ALPHA = 0.1f
+ const val ListHeaderAlpha = 0.1f
const val ONE_WEEK_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000
val LIST_HEADER_ELEVATION = 4.dp
val LIST_HEADER_FONT_SIZE = 16.sp
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListScreen.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListScreen.kt
index 69b9fd0..b5fb73e 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListScreen.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmlist/components/AlarmListScreen.kt
@@ -46,6 +46,7 @@ import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.framework.database.AlarmMapper
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnAddAlarmClick
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnClearAlarmsClick
+import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnClearEmptyAlarmsClick
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnDeleteAlarmClick
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnEditAlarmClick
import com.timilehinaregbesola.mathalarm.presentation.alarmlist.AlarmListEvent.OnUndoDeleteClick
@@ -58,10 +59,8 @@ import com.timilehinaregbesola.mathalarm.utils.Destinations.AppSettings
import com.timilehinaregbesola.mathalarm.utils.Destinations.SettingsSheet
import com.timilehinaregbesola.mathalarm.utils.UiEvent.Navigate
import com.timilehinaregbesola.mathalarm.utils.UiEvent.ShowSnackbar
-import com.timilehinaregbesola.mathalarm.utils.getCalendarFromAlarm
import com.timilehinaregbesola.mathalarm.utils.getTimeLeft
import kotlinx.serialization.json.Json
-import java.util.Calendar
@SuppressLint("UnrememberedMutableState")
@ExperimentalAnimationApi
@@ -116,7 +115,13 @@ fun ListDisplayScreen(
Scaffold(
topBar = {
ListTopAppBar(
- openDialog = { deleteAllAlarmsDialog = true },
+ openDialog = {
+ if (alarmList.isNotEmpty()) {
+ deleteAllAlarmsDialog = true
+ } else {
+ viewModel.onEvent(OnClearEmptyAlarmsClick)
+ }
+ },
onSettingsClick = {
backstack.add(AppSettings)
},
@@ -155,7 +160,6 @@ fun ListDisplayScreen(
val alarmSetText = strings.alarmSet
AlarmListContent(
alarmList = alarmList,
- calendar = viewModel.calender.getCurrentCalendar(),
darkTheme = darkTheme,
onEditAlarm = {
isLoading = true
@@ -180,19 +184,10 @@ fun ListDisplayScreen(
checkPermissionAndPerformAction(
value = alarmPermission.hasExactAlarmPermission(),
action = {
- val calender = viewModel.calender.getCurrentCalendar()
viewModel.scheduleAlarm(
alarm = curAlarm,
reschedule = b,
- message = "$alarmSetText ${
- curAlarm.getTimeLeft(
- getCalendarFromAlarm(
- curAlarm,
- calender
- ).timeInMillis,
- calender,
- )
- }",
+ message = "$alarmSetText ${curAlarm.getTimeLeft()}",
)
},
onPermissionAbsent = { showPermissionDialog = true },
@@ -233,7 +228,6 @@ fun ListDisplayScreen(
@Composable
private fun AlarmListContent(
alarmList: List,
- calendar: Calendar,
darkTheme: Boolean,
onEditAlarm: (Alarm) -> Unit,
onUpdateAlarm: (Alarm) -> Unit,
@@ -254,8 +248,7 @@ private fun AlarmListContent(
ListHeader(
enabled = alarmList.any { it.isOn },
alarmList = alarmList,
- calendar = calendar,
- isDark = darkTheme,
+ isDark = darkTheme
)
}
items(
@@ -325,7 +318,6 @@ private fun AlarmListScreenPreview() {
MathAlarmTheme {
AlarmListContent(
alarmList = listOf(Alarm(), Alarm(alarmId = 1L)),
- calendar = Calendar.getInstance(),
darkTheme = false,
onEditAlarm = {},
onUpdateAlarm = {},
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/AlarmMathViewModel.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/AlarmMathViewModel.kt
index 7614396..6a8e6dd 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/AlarmMathViewModel.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/AlarmMathViewModel.kt
@@ -2,8 +2,6 @@ package com.timilehinaregbesola.mathalarm.presentation.alarmmath
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
@@ -13,7 +11,10 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
@@ -22,8 +23,8 @@ class AlarmMathViewModel @Inject constructor(
private val usecases: Usecases,
val audioPlayer: AudioPlayer,
) : ViewModel() {
- private val _state = MutableLiveData(ToneState.Stopped())
- val state: LiveData = _state
+ private val _state = MutableStateFlow(ToneState.Stopped())
+ val state: StateFlow = _state.asStateFlow()
private val _answerText = mutableStateOf("")
val answerText: State = _answerText
private val _eventFlow = MutableSharedFlow()
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/components/MathScreen.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/components/MathScreen.kt
index cb1368f..79e2170 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/components/MathScreen.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmmath/components/MathScreen.kt
@@ -37,7 +37,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -123,7 +123,7 @@ fun MathScreen(
SnackbarHostState()
}
val keyboardController = LocalSoftwareKeyboardController.current
- val toneState by viewModel.state.observeAsState()
+ val toneState by viewModel.state.collectAsState()
val progress by remember(viewModel.audioPlayer.currentPosition) {
mutableFloatStateOf(
if (toneState is Countdown) {
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/AlarmSettingsViewModel.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/AlarmSettingsViewModel.kt
index 91fd18a..ce9c8d1 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/AlarmSettingsViewModel.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/AlarmSettingsViewModel.kt
@@ -1,26 +1,29 @@
package com.timilehinaregbesola.mathalarm.presentation.alarmsettings
import android.media.RingtoneManager
-import androidx.compose.runtime.*
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.text.input.TextFieldValue
-import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.framework.Usecases
-import com.timilehinaregbesola.mathalarm.utils.getDayOfWeek
import com.timilehinaregbesola.mathalarm.utils.getFormatTime
+import com.timilehinaregbesola.mathalarm.utils.initLocalDateTimeInSystemZone
+import com.timilehinaregbesola.mathalarm.utils.toIndex
import dagger.hilt.android.lifecycle.HiltViewModel
-import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
-import java.util.*
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.datetime.LocalDateTime
import javax.inject.Inject
@HiltViewModel
class AlarmSettingsViewModel @Inject constructor(
private val usecases: Usecases,
- savedStateHandle: SavedStateHandle,
) : ViewModel() {
private var isNewAlarm: Boolean? = null
@@ -74,9 +77,6 @@ class AlarmSettingsViewModel @Inject constructor(
}
}
is AddEditAlarmEvent.OnTestClick -> {
-// runBlocking {
-// usecases.addAlarm(createAlarm())
-// }
viewModelScope.launch {
_eventFlow.emit(UiEvent.TestAlarm(createAlarm()))
}
@@ -106,7 +106,7 @@ class AlarmSettingsViewModel @Inject constructor(
_dayChooser.value = event.value
}
is AddEditAlarmEvent.OnDifficultyChange -> {
- _difficulty.value = event.value
+ _difficulty.intValue = event.value
}
is AddEditAlarmEvent.OnToneChange -> {
_tone.value = event.value
@@ -135,13 +135,7 @@ class AlarmSettingsViewModel @Inject constructor(
isSaved = _isSaved.value,
)
- private fun initCalendar(alarm: Alarm): Calendar {
- val cal = Calendar.getInstance()
- cal[Calendar.HOUR_OF_DAY] = alarm.hour
- cal[Calendar.MINUTE] = alarm.minute
- cal[Calendar.SECOND] = 0
- return cal
- }
+ private fun initDateTime(alarm: Alarm): LocalDateTime = alarm.initLocalDateTimeInSystemZone()
fun setAlarm(curAlarm: Alarm) {
if (currentAlarmId == null) {
@@ -155,9 +149,8 @@ class AlarmSettingsViewModel @Inject constructor(
if (alarm.repeatDays == "FFFFFFF") {
isNewAlarm = true
val sb = StringBuilder("FFFFFFF")
- val cal = initCalendar(alarm)
- val dayOfTheWeek =
- getDayOfWeek(cal[Calendar.DAY_OF_WEEK])
+ val dateTime = initDateTime(alarm)
+ val dayOfTheWeek = dateTime.date.dayOfWeek.toIndex()
sb.setCharAt(dayOfTheWeek, 'T')
_dayChooser.value = sb.toString()
} else {
@@ -166,7 +159,7 @@ class AlarmSettingsViewModel @Inject constructor(
}
_repeatWeekly.value = alarm.repeat
_vibrate.value = alarm.vibrate
- _difficulty.value = alarm.difficulty
+ _difficulty.intValue = alarm.difficulty
if (alarm.alarmTone == "") {
_tone.value = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM).toString()
} else {
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/AlarmBottomSheet.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/AlarmBottomSheet.kt
index 4afac68..06b7c3f 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/AlarmBottomSheet.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/AlarmBottomSheet.kt
@@ -84,7 +84,6 @@ import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.A
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TEST_BUTTON_FONT_SIZE
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TIME_CARD_CORNER_SIZE
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TIME_CARD_HEIGHT
-import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TIME_PATTERN
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TIME_TEXT_FONT_SIZE
import com.timilehinaregbesola.mathalarm.presentation.alarmsettings.components.AlarmBottomSheet.TIME_TEXT_PADDING
import com.timilehinaregbesola.mathalarm.presentation.ui.MathAlarmTheme
@@ -101,9 +100,11 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.format
+import kotlinx.datetime.format.char
import kotlinx.serialization.json.Json
import timber.log.Timber
-import java.time.format.DateTimeFormatter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -113,7 +114,9 @@ fun AlarmBottomSheet(
darkTheme: Boolean,
alarm: AlarmEntity,
) {
- viewModel.setAlarm(AlarmMapper().mapToDomainModel(alarm))
+ LaunchedEffect(Unit) {
+ viewModel.setAlarm(AlarmMapper().mapToDomainModel(alarm))
+ }
val scaffoldState = rememberBottomSheetScaffoldState()
var showTimePickerDialog by remember { mutableStateOf(false) }
var showConfirmationDialog by remember { mutableStateOf(false) }
@@ -264,13 +267,19 @@ fun AlarmBottomSheet(
showTimePickerDialog = false
},
onConfirm = { newTime ->
- val dtf = DateTimeFormatter.ofPattern(TIME_PATTERN)
+ val tf = LocalTime.Format {
+ amPmHour()
+ char(':')
+ minute()
+ char(' ')
+ amPmMarker("AM", "PM")
+ }
viewModel.onEvent(
AddEditAlarmEvent.ChangeTime(
TimeState(
hour = newTime.hour,
minute = newTime.minute,
- formattedTime = newTime.format(dtf).toString(),
+ formattedTime = newTime.format(tf)
),
),
)
@@ -461,9 +470,7 @@ private fun SheetActionButtons(
modifier = Modifier
.padding(top = MaterialTheme.spacing.large)
.fillMaxWidth(),
- onClick = {
- onTestClick()
- },
+ onClick = onTestClick,
colors = buttonColors(
containerColor = unSelectedDay,
contentColor = Black,
@@ -478,9 +485,7 @@ private fun SheetActionButtons(
modifier = Modifier
.padding(top = SAVE_BUTTON_TOP_PADDING)
.fillMaxWidth(),
- onClick = {
- onSaveClick()
- },
+ onClick = onSaveClick,
colors = buttonColors(containerColor = MaterialTheme.colorScheme.secondary),
) {
Text(
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/TimePickerDialog.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/TimePickerDialog.kt
index e3895a9..f12bb71 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/TimePickerDialog.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/presentation/alarmsettings/components/TimePickerDialog.kt
@@ -47,8 +47,7 @@ import androidx.compose.ui.window.DialogProperties
import cafe.adriel.lyricist.strings
import com.timilehinaregbesola.mathalarm.presentation.ui.MathAlarmTheme
import com.timilehinaregbesola.mathalarm.presentation.ui.darkPrimary
-import java.time.LocalTime
-import java.util.Calendar
+import kotlinx.datetime.LocalTime
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -60,13 +59,10 @@ fun TimePickerDialog(
darkTheme: Boolean = false,
) {
- val time = Calendar.getInstance()
- time.timeInMillis = System.currentTimeMillis()
-
var mode: DisplayMode by remember { mutableStateOf(DisplayMode.Picker) }
fun onConfirmClicked() {
- val currentTime = LocalTime.of(timeState.hour, timeState.minute)
+ val currentTime = LocalTime(timeState.hour, timeState.minute)
onConfirm(currentTime)
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/AlarmUtil.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/AlarmUtil.kt
index e18ab00..a977bdd 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/AlarmUtil.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/AlarmUtil.kt
@@ -1,8 +1,17 @@
package com.timilehinaregbesola.mathalarm.utils
-import android.text.format.DateFormat
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
-import java.util.*
+import kotlinx.datetime.DatePeriod
+import kotlinx.datetime.DayOfWeek
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.LocalTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.plus
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
+import kotlin.time.Instant
const val SUN = 0
const val MON = 1
@@ -16,104 +25,170 @@ const val MEDIUM = 1
const val HARD = 2
val days = listOf("S", "M", "T", "W", "T", "F", "S")
-val fullDays = listOf("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
-
-// Get the formatted time (example: 12:00 AM)
-fun Alarm.getFormatTime(): CharSequence? {
- val cal = Calendar.getInstance()
- cal[Calendar.HOUR_OF_DAY] = hour
- cal[Calendar.MINUTE] = minute
- return DateFormat.format("hh:mm a", cal)
+val fullDays = listOf(
+ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+)
+
+/**
+ * Returns a 12-hour "hh:mm AM/PM" string for this Alarm's hour/minute.
+ */
+fun Alarm.getFormatTime(): String {
+ val isAm = hour < 12
+ val hour12 = when {
+ hour == 0 -> 12
+ hour == 12 -> 12
+ hour > 12 -> hour - 12
+ else -> hour
+ }
+ val minutePadded = minute.toString().padStart(2, '0')
+ val amPm = if (isAm) "AM" else "PM"
+ return "%02d:%s %s".format(hour12, minutePadded, amPm)
}
-fun Alarm.getTime(): Calendar {
- val cal = Calendar.getInstance()
- cal[Calendar.HOUR_OF_DAY] = hour
- cal[Calendar.MINUTE] = minute
- return cal
+/**
+ * Extension function for [DayOfWeek] to get its corresponding index (0-6).
+ */
+fun DayOfWeek.toIndex(): Int = when (this) {
+ DayOfWeek.SUNDAY -> SUN
+ DayOfWeek.MONDAY -> MON
+ DayOfWeek.TUESDAY -> TUE
+ DayOfWeek.WEDNESDAY -> WED
+ DayOfWeek.THURSDAY -> THU
+ DayOfWeek.FRIDAY -> FRI
+ DayOfWeek.SATURDAY -> SAT
}
-fun Alarm.initCalendar(): Calendar {
- val cal = Calendar.getInstance()
- cal[Calendar.HOUR_OF_DAY] = hour
- cal[Calendar.MINUTE] = minute
- cal[Calendar.SECOND] = 0
- return cal
+/**
+ * Like getTodayDateTimeInSystemZone(), but enforces second=0.
+ */
+@OptIn(ExperimentalTime::class)
+fun Alarm.initLocalDateTimeInSystemZone(): LocalDateTime {
+ val nowInstant = Clock.System.now()
+ val tz = TimeZone.currentSystemDefault()
+ val today = nowInstant.toLocalDateTime(tz).date
+ return LocalDateTime(
+ date = today,
+ time = LocalTime(hour, minute, 0)
+ )
}
-fun Alarm.getTimeLeft(time: Long, cal: Calendar): String {
- val message: String
- val calender = getCalendarFromAlarm(alarm = this, cal = cal)
- val today = getDayOfWeek(calender[Calendar.DAY_OF_WEEK])
- var i: Int
- val lastAlarmDay: Int
- val nextAlarmDay: Int
- if (System.currentTimeMillis() > time) {
- nextAlarmDay = if (today + 1 == 7) 0 else today + 1
- lastAlarmDay = today
- } else {
- nextAlarmDay = today
- lastAlarmDay = if (today - 1 == -1) 6 else today - 1
- }
- i = nextAlarmDay
- while (i != lastAlarmDay) {
- if (i == 7) {
- i = 0
- }
- if (repeatDays[i] == 'T') {
- break
+/**
+ * Calculate the next time an alarm will go off.
+ *
+ * @param alarm The alarm to calculate the next time for
+ * @param timeZone The timezone to use for the calculation
+ * @return The next time the alarm will go off as an Instant, or null if the alarm has no repeat days set
+ */
+@OptIn(ExperimentalTime::class)
+fun calculateNextAlarmTime(alarm: Alarm, timeZone: TimeZone = TimeZone.currentSystemDefault()): Instant? {
+ val nowInstant = Clock.System.now()
+ val localNow = nowInstant.toLocalDateTime(timeZone)
+ val todayDate = localNow.date
+
+ // Check if the alarm has any repeat days set
+ val hasRepeatDays = alarm.repeatDays.contains('T')
+
+ if (!hasRepeatDays) {
+ // If alarm doesn't have any repeat days set, just check today
+ val alarmDateTime = LocalDateTime(
+ date = todayDate,
+ time = LocalTime(alarm.hour, alarm.minute, 0)
+ )
+ var candidateInstant = alarmDateTime.toInstant(timeZone)
+
+ // If the alarm time is in the past, add one week (7 days)
+ if (candidateInstant < nowInstant) {
+ val oneWeek = DatePeriod(days = 7)
+ candidateInstant = candidateInstant.plus(oneWeek, timeZone)
}
- i++
- }
- if (i < today || i == today && calender.timeInMillis < System.currentTimeMillis()) {
- val daysUntilAlarm: Int = SAT - today + 1 + i
- calender.add(Calendar.DAY_OF_YEAR, daysUntilAlarm)
+
+ return candidateInstant
} else {
- val daysUntilAlarm = i - today
- calender.add(Calendar.DAY_OF_YEAR, daysUntilAlarm)
- }
- val alarmTime = calender.timeInMillis
- val remainderTime = alarmTime - System.currentTimeMillis()
- val minutes = (remainderTime / (1000 * 60) % 60).toInt()
- val hours = (remainderTime / (1000 * 60 * 60) % 24).toInt()
- val days = (remainderTime / (1000 * 60 * 60 * 24)).toInt()
- val mString = if (minutes == 1) "minute" else "minutes"
- val hString = if (hours == 1) "hour" else "hours"
- val dString = if (days == 1) "day" else "days"
- message = if (days == 0) {
- if (hours == 0) {
- ("$minutes $mString")
+ // For alarms with repeat days, find the next occurrence based on repeat days
+
+ // Get the current day of week (0..6)
+ val currentDayIndex = todayDate.dayOfWeek.toIndex()
+
+ // Check if the alarm is set for the current day
+ val isSetForToday = alarm.repeatDays.getOrNull(currentDayIndex) == 'T'
+
+ // Create a LocalDateTime for the alarm time today
+ val alarmTimeToday = LocalDateTime(
+ date = todayDate,
+ time = LocalTime(alarm.hour, alarm.minute, 0)
+ )
+ val alarmInstantToday = alarmTimeToday.toInstant(timeZone)
+
+ // If the alarm is set for today and the time hasn't passed yet, use today's time
+ if (isSetForToday && alarmInstantToday > nowInstant) {
+ return alarmInstantToday
} else {
- ("$hours $hString $minutes $mString")
+ // Otherwise, find the next occurrence based on repeat days
+ // Find the earliest day such that:
+ // - repeatDays[dayIndex] == 'T'
+ // - The alarm time for that day is in the future
+
+ // First, try to find the next occurrence within the next 7 days
+ for (offset in 1..7) {
+ val nextDate = todayDate.plus(DatePeriod(days = offset))
+ val nextDayIndex = nextDate.dayOfWeek.toIndex()
+
+ if (alarm.repeatDays.getOrNull(nextDayIndex) == 'T') {
+ val candidateDateTime = LocalDateTime(
+ date = nextDate,
+ time = LocalTime(alarm.hour, alarm.minute, 0)
+ )
+ return candidateDateTime.toInstant(timeZone)
+ }
+ }
+
+ // If no future occurrence was found, and the alarm is set for today but the time has passed,
+ // use today's time + 1 week
+ if (isSetForToday) {
+ val oneWeek = DatePeriod(days = 7)
+ return alarmInstantToday.plus(oneWeek, timeZone)
+ }
}
- } else {
- (
- " $days $dString $hours $hString $minutes $mString "
- )
}
- return message
-}
-fun getCalendarFromAlarm(alarm: Alarm, cal: Calendar): Calendar {
- cal[Calendar.DAY_OF_WEEK] = alarm.repeatDays.toList().indexOfFirst { it == 'T' } + 1
- cal[Calendar.HOUR_OF_DAY] = alarm.hour
- cal[Calendar.MINUTE] = alarm.minute
- cal[Calendar.SECOND] = 0
- return cal
+ // If no future day matched (repeatDays all 'F'), return null
+ return null
}
-fun getDayOfWeek(day: Int): Int {
- val dayOfWeek: Int
- val errorValue = 8
- dayOfWeek = when (day) {
- Calendar.SUNDAY -> SUN
- Calendar.MONDAY -> MON
- Calendar.TUESDAY -> TUE
- Calendar.WEDNESDAY -> WED
- Calendar.THURSDAY -> THU
- Calendar.FRIDAY -> FRI
- Calendar.SATURDAY -> SAT
- else -> return errorValue
+/**
+ * Compute "time left until next alarm" exactly as your old Calendar‐based function did,
+ * but using kotlinx-datetime under the hood. Returns a string like:
+ *
+ * • "5 hours 3 minutes"
+ * • "1 day 2 hours 15 minutes"
+ * • "45 minutes"
+ * • "0 minutes" (if repeatDays is all 'F')
+ *
+ * We interpret repeatDays[0]=='T' as Sunday, [1]=='T' as Monday, …, [6]=='T' as Saturday.
+ */
+@OptIn(ExperimentalTime::class)
+fun Alarm.getTimeLeft(): String {
+ val nowInstant = Clock.System.now()
+ val chosenInstant = calculateNextAlarmTime(this)
+
+ // If no future day matched (repeatDays all 'F'), show "0 minutes"
+ if (chosenInstant == null) {
+ return "0 minutes"
+ }
+
+ // Compute the duration between "now" and "chosenInstant" in seconds:
+ val totalSeconds = (chosenInstant - nowInstant).inWholeSeconds
+ val daysPart = (totalSeconds / (60 * 60 * 24)).toInt()
+ val hoursPart = ((totalSeconds % (60 * 60 * 24)) / (60 * 60)).toInt()
+ val minutesPart = ((totalSeconds % (60 * 60)) / 60).toInt()
+
+ val dString = if (daysPart == 1) "day" else "days"
+ val hString = if (hoursPart == 1) "hour" else "hours"
+ val mString = if (minutesPart == 1) "minute" else "minutes"
+
+ return when {
+ daysPart > 0 -> "$daysPart $dString $hoursPart $hString $minutesPart $mString"
+ hoursPart > 0 -> "$hoursPart $hString $minutesPart $mString"
+ else -> "$minutesPart $mString"
}
- return dayOfWeek
}
diff --git a/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/ContextExtensions.kt b/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/ContextExtensions.kt
index 63bdf95..ae7c471 100644
--- a/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/ContextExtensions.kt
+++ b/app/src/main/java/com/timilehinaregbesola/mathalarm/utils/ContextExtensions.kt
@@ -6,20 +6,17 @@ import android.app.Activity
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
-import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.provider.Settings
-import android.view.ContextThemeWrapper
-import android.widget.Toast
-import androidx.annotation.StringRes
import androidx.core.app.ActivityCompat
import androidx.core.app.AlarmManagerCompat
import androidx.core.content.ContextCompat
import timber.log.Timber
-import java.util.Calendar
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
/**
* Sets a alarm using [AlarmManagerCompat] to be triggered based on the given parameter.
@@ -29,27 +26,48 @@ import java.util.Calendar
* @param operation action to perform when the alarm goes off
* @param type type to define how the alarm will behave
*/
+@OptIn(ExperimentalTime::class)
fun Context.setExactAlarm(
triggerAtMillis: Long,
operation: PendingIntent?,
type: Int = AlarmManager.RTC_WAKEUP,
) {
- val currentTime = Calendar.getInstance().timeInMillis
- if (triggerAtMillis <= currentTime) {
- Timber.w("It is not possible to set alarm in the past")
- return
+ var adjustedTriggerTime = triggerAtMillis
+ val currentTime = Clock.System.now().toEpochMilliseconds()
+
+ if (adjustedTriggerTime <= currentTime) {
+ // If the alarm time is in the past, add one week (7 days) to the trigger time
+ Timber.w("Alarm time is in the past, scheduling for next week")
+ val oneWeekInMillis = 7 * 24 * 60 * 60 * 1000L
+ adjustedTriggerTime += oneWeekInMillis
}
if (operation == null) {
- Timber.e("PendingIntent is null")
+ Timber.e("PendingIntent is null, cannot schedule alarm")
return
}
val manager = getAlarmManager()
- manager?.let {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || it.canScheduleExactAlarms()) {
- AlarmManagerCompat.setExactAndAllowWhileIdle(it, type, triggerAtMillis, operation)
- }
+ if (manager == null) {
+ Timber.e("AlarmManager is null, cannot schedule alarm")
+ return
+ }
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !manager.canScheduleExactAlarms()) {
+ Timber.e("Cannot schedule exact alarms - permission not granted on Android S+")
+ return
+ }
+
+ try {
+ AlarmManagerCompat.setExactAndAllowWhileIdle(
+ manager,
+ type,
+ adjustedTriggerTime,
+ operation
+ )
+ Timber.d("Alarm scheduled successfully")
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to schedule alarm")
}
}
@@ -59,23 +77,26 @@ fun Context.setExactAlarm(
* @param operation action to be canceled
*/
fun Context.cancelAlarm(operation: PendingIntent?) {
+ Timber.d("cancelAlarm called with operation=${operation?.hashCode()}")
+
if (operation == null) {
- Timber.e("PendingIntent is null")
+ Timber.e("PendingIntent is null, cannot cancel alarm")
return
}
val manager = getAlarmManager()
- manager?.let { manager.cancel(operation) }
-}
+ if (manager == null) {
+ Timber.e("AlarmManager is null, cannot cancel alarm")
+ return
+ }
-/**
- * Shows a [Toast] with the given message.
- *
- * @param messageId the message String resource id
- * @param duration the Toast duration, if not provided will be set to [Toast.LENGTH_SHORT]
- */
-fun Context.showToast(@StringRes messageId: Int, duration: Int = Toast.LENGTH_SHORT) {
- Toast.makeText(this, messageId, duration).show()
+ try {
+ Timber.d("Canceling alarm with AlarmManager.cancel")
+ manager.cancel(operation)
+ Timber.d("Alarm canceled successfully")
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to cancel alarm")
+ }
}
fun Context.email(
diff --git a/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmInteractorFake.kt b/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmInteractorFake.kt
index 19ec2d1..d470666 100644
--- a/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmInteractorFake.kt
+++ b/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmInteractorFake.kt
@@ -2,13 +2,13 @@ package com.timilehinaregbesola.mathalarm.fake
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.interactors.AlarmInteractor
-import com.timilehinaregbesola.mathalarm.utils.getTime
-import java.util.Calendar
+import com.timilehinaregbesola.mathalarm.utils.initLocalDateTimeInSystemZone
+import kotlinx.datetime.LocalDateTime
class AlarmInteractorFake : AlarmInteractor {
private val alarmMap: MutableMap = mutableMapOf()
override fun schedule(alarm: Alarm, reschedule: Boolean): Boolean {
- alarmMap[alarm.alarmId] = FakeData(reschedule, alarm.getTime())
+ alarmMap[alarm.alarmId] = FakeData(reschedule, alarm.initLocalDateTimeInSystemZone())
return true
}
@@ -20,8 +20,8 @@ class AlarmInteractorFake : AlarmInteractor {
fun clear() = alarmMap.clear()
- fun getAlarmTime(alarmId: Long): Calendar? =
+ fun getAlarmTime(alarmId: Long): LocalDateTime? =
alarmMap[alarmId]?.time
}
-data class FakeData(val reschedule: Boolean, val time: Calendar)
+data class FakeData(val reschedule: Boolean, val time: LocalDateTime)
diff --git a/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmRepositoryFake.kt b/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmRepositoryFake.kt
index fc84127..4c28d6b 100644
--- a/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmRepositoryFake.kt
+++ b/app/src/test/java/com/timilehinaregbesola/mathalarm/fake/AlarmRepositoryFake.kt
@@ -4,15 +4,14 @@ import com.timilehinaregbesola.mathalarm.data.AlarmDataSource
import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
-import java.util.TreeMap
class AlarmRepositoryFake : AlarmDataSource {
- private val alarmMap: TreeMap = TreeMap()
+ private val alarmMap: MutableMap = mutableMapOf()
override suspend fun addAlarm(alarm: Alarm): Long {
val id = if (alarm.alarmId == 0L) {
- alarmMap.lastKey() + 1
+ (alarmMap.keys.maxOrNull() ?: 0L) + 1
} else {
alarm.alarmId
}
@@ -37,7 +36,7 @@ class AlarmRepositoryFake : AlarmDataSource {
override fun getSavedAlarms(): Flow> = flowOf(alarmMap.values.toList().filter { it.isSaved })
- override suspend fun getLatestAlarmFromDatabase(): Alarm? = alarmMap[alarmMap.lastKey()]
+ override suspend fun getLatestAlarmFromDatabase(): Alarm? = alarmMap.values.maxByOrNull { it.alarmId }
override suspend fun findAlarm(id: Long): Alarm? = alarmMap[id]
diff --git a/app/src/test/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationSchedulerTest.kt b/app/src/test/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationSchedulerTest.kt
index 6e84ef9..fb25041 100644
--- a/app/src/test/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationSchedulerTest.kt
+++ b/app/src/test/java/com/timilehinaregbesola/mathalarm/notification/AlarmNotificationSchedulerTest.kt
@@ -5,8 +5,8 @@ import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
+import kotlinx.datetime.LocalDateTime
import org.junit.Before
-import java.util.Calendar
class AlarmNotificationSchedulerTest {
@@ -18,20 +18,20 @@ class AlarmNotificationSchedulerTest {
@Before
fun setUp() {
- val cal = Calendar.getInstance()
+ val now = LocalDateTime(2023, 1, 1, 10, 30) // Example LocalDateTime
mockkStatic("com.timilehinaregbesola.mathalarm.utils.extension-context")
every { mockAlarm.repeatDays } returns "FFFTFFF"
every { mockAlarm.alarmId } returns 22L
every { mockAlarm.repeat } returns false
- every { mockAlarm.newCal } returns cal
- every { mockAlarm.hour } returns cal[Calendar.HOUR_OF_DAY]
- every { mockAlarm.minute } returns cal[Calendar.MINUTE]
+ every { mockAlarm.newDateTime } returns now
+ every { mockAlarm.hour } returns now.hour
+ every { mockAlarm.minute } returns now.minute
}
// @Test
// fun `check if alarm scheduled is valid`() {
// scheduler.scheduleAlarm(mockAlarm, false)
-// verify { mockContext.setAlarm(mockAlarm.newCal.time.time, any()) }
+// verify { mockContext.setExactAlarm(mockAlarm.newDateTime.time.toNanosecondOfDay(), any()) }
// }
//
// @Test
diff --git a/app/src/test/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarmTest.kt b/app/src/test/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarmTest.kt
index 707712b..77d8fb8 100644
--- a/app/src/test/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarmTest.kt
+++ b/app/src/test/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarmTest.kt
@@ -5,7 +5,7 @@ import com.timilehinaregbesola.mathalarm.domain.model.Alarm
import com.timilehinaregbesola.mathalarm.fake.AlarmInteractorFake
import com.timilehinaregbesola.mathalarm.fake.AlarmRepositoryFake
import com.timilehinaregbesola.mathalarm.fake.NotificationInteractorFake
-import com.timilehinaregbesola.mathalarm.provider.CalendarProviderImpl
+import com.timilehinaregbesola.mathalarm.provider.DateTimeProviderImpl
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -21,7 +21,7 @@ class SnoozeAlarmTest {
private val notificationInteractor = NotificationInteractorFake()
- private val calendarProvider = CalendarProviderImpl()
+ private val calendarProvider = DateTimeProviderImpl()
private val addAlarmUseCase = AddAlarm(alarmRepository)
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 14b17a7..1d3a975 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -12,4 +12,5 @@ dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(libs.coroutines.android)
implementation(libs.timber)
+ implementation(libs.kotlinx.datetime)
}
diff --git a/core/src/main/java/com/timilehinaregbesola/mathalarm/domain/model/Alarm.kt b/core/src/main/java/com/timilehinaregbesola/mathalarm/domain/model/Alarm.kt
index bed0768..b61d468 100644
--- a/core/src/main/java/com/timilehinaregbesola/mathalarm/domain/model/Alarm.kt
+++ b/core/src/main/java/com/timilehinaregbesola/mathalarm/domain/model/Alarm.kt
@@ -1,15 +1,19 @@
package com.timilehinaregbesola.mathalarm.domain.model
-import java.util.*
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
-data class Alarm(
+data class Alarm @OptIn(ExperimentalTime::class) constructor(
var alarmId: Long = 0L,
- val newCal: Calendar = Calendar.getInstance(),
+ val newDateTime: LocalDateTime = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()),
- val newHour: Int = newCal[Calendar.HOUR_OF_DAY],
+ val newHour: Int = newDateTime.hour,
- val newMinute: Int = newCal[Calendar.MINUTE],
+ val newMinute: Int = newDateTime.minute,
var hour: Int = newHour,
diff --git a/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProvider.kt b/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProvider.kt
index 918a492..486bf77 100644
--- a/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProvider.kt
+++ b/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProvider.kt
@@ -1,16 +1,16 @@
package com.timilehinaregbesola.mathalarm.provider
-import java.util.Calendar
+import kotlinx.datetime.LocalDateTime
/**
* Provide the date and time to be used on the alarm use cases, respecting the Inversion of Control.
*/
-interface CalendarProvider {
+interface DateTimeProvider {
/**
- * Gets the current [Calendar].
+ * Gets the current [LocalDateTime] in the system default time zone.
*
- * @return the current [Calendar]
+ * @return the current [LocalDateTime]
*/
- fun getCurrentCalendar(): Calendar
+ fun getCurrentDateTime(): LocalDateTime
}
diff --git a/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProviderImpl.kt b/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProviderImpl.kt
index 0fb2256..020e583 100644
--- a/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProviderImpl.kt
+++ b/core/src/main/java/com/timilehinaregbesola/mathalarm/provider/CalendarProviderImpl.kt
@@ -1,16 +1,23 @@
package com.timilehinaregbesola.mathalarm.provider
-import java.util.Calendar
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import kotlin.time.Clock
+import kotlin.time.ExperimentalTime
+
/**
* Provide the date and time to be used on the alarm use cases, respecting the Inversion of Control.
*/
-class CalendarProviderImpl : CalendarProvider {
+class DateTimeProviderImpl : DateTimeProvider {
/**
- * Gets the current [Calendar].
+ * Gets the current [LocalDateTime] in the system default time zone.
*
- * @return the current [Calendar]
+ * @return the current [LocalDateTime]
*/
- override fun getCurrentCalendar(): Calendar = Calendar.getInstance()
+ @OptIn(ExperimentalTime::class)
+ override fun getCurrentDateTime(): LocalDateTime =
+ Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
}
diff --git a/core/src/main/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarm.kt b/core/src/main/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarm.kt
index 7d5f9f8..cb427ed 100644
--- a/core/src/main/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarm.kt
+++ b/core/src/main/java/com/timilehinaregbesola/mathalarm/usecases/SnoozeAlarm.kt
@@ -3,14 +3,20 @@ package com.timilehinaregbesola.mathalarm.usecases
import com.timilehinaregbesola.mathalarm.data.AlarmRepository
import com.timilehinaregbesola.mathalarm.interactors.AlarmInteractor
import com.timilehinaregbesola.mathalarm.interactors.NotificationInteractor
-import com.timilehinaregbesola.mathalarm.provider.CalendarProvider
-import java.util.*
+import com.timilehinaregbesola.mathalarm.provider.DateTimeProvider
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDateTime
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.plus
+import kotlinx.datetime.toInstant
+import kotlinx.datetime.toLocalDateTime
+import kotlin.time.ExperimentalTime
/**
* Use case to snooze an alarm.
*/
class SnoozeAlarm(
- private val calendarProvider: CalendarProvider,
+ private val dateTimeProvider: DateTimeProvider,
private val notificationInteractor: NotificationInteractor,
private val alarmInteractor: AlarmInteractor,
private val alarmRepository: AlarmRepository
@@ -28,18 +34,22 @@ class SnoozeAlarm(
require(minutes > 0) { "The delay minutes must be positive" }
val alarm = alarmRepository.findAlarm(alarmId) ?: return
- val snoozedTime = getSnoozedAlarm(calendarProvider.getCurrentCalendar(), minutes)
+ val snoozedTime = getSnoozedDateTime(dateTimeProvider.getCurrentDateTime(), minutes)
alarm.apply {
- hour = snoozedTime[Calendar.HOUR_OF_DAY]
- minute = snoozedTime[Calendar.MINUTE]
+ hour = snoozedTime.hour
+ minute = snoozedTime.minute
}
alarmInteractor.schedule(alarm, alarm.repeat)
notificationInteractor.dismiss(alarmId)
-// logger.debug("Task snoozed in $minutes minutes")
}
- private fun getSnoozedAlarm(calendar: Calendar, minutes: Int): Calendar =
- calendar.apply { add(Calendar.MINUTE, minutes) }
+ @OptIn(ExperimentalTime::class)
+ private fun getSnoozedDateTime(dateTime: LocalDateTime, minutes: Int): LocalDateTime {
+ val tz = TimeZone.currentSystemDefault()
+ val instant = dateTime.toInstant(tz)
+ val newInstant = instant.plus(minutes.toLong(), DateTimeUnit.MINUTE)
+ return newInstant.toLocalDateTime(tz)
+ }
companion object {
private const val DEFAULT_SNOOZE = 5
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index d0b35af..4777b85 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -37,6 +37,7 @@ navigation3 = "1.0.0-alpha08"
nav3Material = "1.0.0-SNAPSHOT"
lifecycleViewmodel = "1.0.0-SNAPSHOT"
appcompat = "1.7.1"
+datetime = "0.7.1"
[libraries]
android-material = { module = "com.google.android.material:material", version.ref = "material" }
@@ -85,6 +86,7 @@ lyricist-processor = { module = "cafe.adriel.lyricist:lyricist-processor", versi
mockk = { module = "io.mockk:mockk", version.ref = "test_mockk" }
plugin-ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
[plugins]
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }