Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .idea/androidTestResultsUserPreferences.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -89,8 +89,8 @@ object AppModule {

@Provides
@Singleton
fun provideCalenderProvider(): CalendarProvider {
return CalendarProviderImpl()
fun provideCalenderProvider(): DateTimeProvider {
return DateTimeProviderImpl()
}

@Provides
Expand Down Expand Up @@ -123,7 +123,7 @@ object AppModule {
repository: AlarmRepository,
alarmInteractor: AlarmInteractor,
notificationInteractor: NotificationInteractor,
calendarProvider: CalendarProvider,
calendarProvider: DateTimeProvider,
scheduleNextAlarm: ScheduleNextAlarm
): Usecases {
return Usecases(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<PendingIntent> = ArrayList()
val time: MutableList<Calendar> = ArrayList()
val timeInstants: MutableList<Instant> = 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
Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -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,
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Loading