From da0938da0fd4ad8ef2acac94139a6a2fbf66b5b4 Mon Sep 17 00:00:00 2001 From: Hafizzle Date: Wed, 15 Nov 2023 07:56:55 -0500 Subject: [PATCH 1/9] Reminder logic updates Check for upcoming reminder and/or if reminder is happening today. Also - error on the side of showing a reminder --- .../habitica/helpers/TaskAlarmManager.kt | 17 +++-- .../android/habitica/models/tasks/Task.kt | 62 +++++++++++-------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index 909c1f83d..bb500c2dd 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -83,7 +83,7 @@ class TaskAlarmManager( private fun setTimeForDailyReminder(remindersItem: RemindersItem?, task: Task): RemindersItem? { val newTime = task.getNextReminderOccurrence(remindersItem, context) - remindersItem?.time = newTime?.withZoneSameLocal(ZoneId.systemDefault())?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + remindersItem?.time = newTime?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) return remindersItem } @@ -95,14 +95,13 @@ class TaskAlarmManager( * which is indicated by first nextDue being null (As the alarm is created before the API returns nextDue times) */ private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?) { + if (remindersItem == null) return + val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() - val zonedTime = remindersItem?.getLocalZonedDateTimeInstant() - if (remindersItem == null || - (reminderItemTask.type == TaskType.DAILY && zonedTime?.isBefore(now) == true && reminderItemTask.nextDue?.firstOrNull() != null) || - (reminderItemTask.type == TaskType.TODO && zonedTime?.isBefore(now) == true || zonedTime == null) - ) { - return - } + val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant() + + if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) return + val intent = Intent(context, TaskReceiver::class.java) intent.action = remindersItem.id @@ -129,7 +128,7 @@ class TaskAlarmManager( withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT) ) - setAlarm(context, zonedTime.toEpochMilli(), sender) + setAlarm(context, reminderZonedTime.toEpochMilli(), sender) } private fun removeAlarmForRemindersItem(remindersItem: RemindersItem) { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index edbe560a0..ea2514e3c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -344,38 +344,46 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { if (remindersItem == null) { return null } + val reminderTime = remindersItem.time?.parseToZonedDateTime() val zonedDateTimeNow = ZonedDateTime.now() - return if (nextDue?.firstOrNull() != null && (!isDisplayedActive || reminderTime?.isBefore(zonedDateTimeNow) == true) - ) { - val repeatingDays = repeat?.dayStrings(context) - val isScheduledForToday = repeatingDays?.find { day -> day == zonedDateTimeNow.dayOfWeekString() } - if (isScheduledForToday != null) { - val currentDateTime = LocalDateTime.now() - val updatedDateTime: LocalDateTime = LocalDateTime.of( - currentDateTime.year, - currentDateTime.month, - currentDateTime.dayOfMonth, - reminderTime?.hour ?: 0, - reminderTime?.minute ?: 0 - ) - updatedDateTime.atZone(ZoneId.systemDefault()) - } else { - val nextDate = LocalDateTime.ofInstant(nextDue?.firstOrNull()?.toInstant(), ZoneId.systemDefault()) - val updatedDateTime: LocalDateTime = LocalDateTime.of( - nextDate.year, - nextDate.month, - nextDate.dayOfMonth, - reminderTime?.hour ?: 0, - reminderTime?.minute ?: 0 - ) - updatedDateTime.atZone(ZoneId.systemDefault()) - } - } else { - return reminderTime + + // Check if the reminder is scheduled to repeat today + val repeatingDays = repeat?.dayStrings(context) + val isScheduledForToday = repeatingDays?.find { day -> day == zonedDateTimeNow.dayOfWeekString() } + // Check if reminder time already passed + val isReminderTimePassed = reminderTime?.isBefore(zonedDateTimeNow) == true + if (isScheduledForToday != null && !isReminderTimePassed) { + val currentDateTime = LocalDateTime.now() + val updatedDateTime: LocalDateTime = LocalDateTime.of( + currentDateTime.year, + currentDateTime.month, + currentDateTime.dayOfMonth, + reminderTime?.hour ?: 0, + reminderTime?.minute ?: 0 + ) + return updatedDateTime.atZone(ZoneId.systemDefault()) } + + + // If the reminder is not scheduled to repeat today, use the first upcoming nextDue date + val today = LocalDate.now() + // Filter out the dates that are in the past + val futureNextDues = nextDue?.filter { nextDueDate -> nextDueDate.toInstant().atZone(ZonedDateTime.now().zone).toLocalDate().isAfter(today) } + val earliestFutureDate = futureNextDues?.minByOrNull { it } + if (earliestFutureDate != null) { + return earliestFutureDate.toInstant() + .atZone(ZonedDateTime.now().zone) + .withHour(reminderTime?.hour ?: 0) + .withMinute(reminderTime?.minute ?: 0) + } + + + // If there are no upcoming nextDue dates, use the first upcoming reminder time + return reminderTime } + fun parseMarkdown() { parsedText = MarkdownParser.parseMarkdown(text) parsedNotes = MarkdownParser.parseMarkdown(notes) From 2b6e49f6f59877c89e437c13909dd08e1e8dc1e5 Mon Sep 17 00:00:00 2001 From: Hafizzle Date: Tue, 21 Nov 2023 00:33:24 -0500 Subject: [PATCH 2/9] Error on the side of showing reminder (Even if reminder time passed) --- .../java/com/habitrpg/android/habitica/models/tasks/Task.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index ea2514e3c..091688240 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -351,9 +351,7 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { // Check if the reminder is scheduled to repeat today val repeatingDays = repeat?.dayStrings(context) val isScheduledForToday = repeatingDays?.find { day -> day == zonedDateTimeNow.dayOfWeekString() } - // Check if reminder time already passed - val isReminderTimePassed = reminderTime?.isBefore(zonedDateTimeNow) == true - if (isScheduledForToday != null && !isReminderTimePassed) { + if (isScheduledForToday != null) { val currentDateTime = LocalDateTime.now() val updatedDateTime: LocalDateTime = LocalDateTime.of( currentDateTime.year, From 19d04147d2a88b02236a80a7e0856c5f67d44607 Mon Sep 17 00:00:00 2001 From: Hafizzle Date: Mon, 27 Nov 2023 23:22:43 -0500 Subject: [PATCH 3/9] Schedule reminders logic updates --- .../extensions/ZonedDateExtensions.kt | 63 +++++++++ .../habitica/helpers/TaskAlarmManager.kt | 89 ++++++++---- .../android/habitica/models/tasks/Task.kt | 131 ++++++++++++------ 3 files changed, 215 insertions(+), 68 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt index a63dd8cb3..9c4d19f64 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt @@ -1,5 +1,7 @@ package com.habitrpg.android.habitica.extensions +import com.habitrpg.android.habitica.models.tasks.Days +import com.habitrpg.shared.habitica.models.tasks.Frequency import java.time.DayOfWeek import java.time.LocalDateTime import java.time.ZoneId @@ -7,6 +9,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatterBuilder import java.time.format.TextStyle +import java.time.temporal.ChronoUnit import java.time.temporal.TemporalAccessor import java.util.Date import java.util.Locale @@ -42,3 +45,63 @@ fun formatter(): DateTimeFormatter = .append(DateTimeFormatter.ISO_LOCAL_TIME) .appendPattern("[XX]") .toFormatter() + + +fun ZonedDateTime.matchesDailyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { + val daysBetween = ChronoUnit.DAYS.between(startDate.toLocalDate(), this.toLocalDate()) + return daysBetween % everyX == 0L +} + +fun ZonedDateTime.matchesWeeklyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { + val weeksBetween = ChronoUnit.WEEKS.between(startDate.toLocalDate(), this.toLocalDate()) + return weeksBetween % everyX == 0L +} + +fun ZonedDateTime.matchesMonthlyInterval(startDate: ZonedDateTime, everyX: Int, dayOfMonth: Int): Boolean { + val monthsBetween = ChronoUnit.MONTHS.between(startDate.toLocalDate(), this.toLocalDate()) + return this.dayOfMonth == dayOfMonth && monthsBetween % everyX == 0L +} + +fun ZonedDateTime.matchesYearlyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { + val yearsBetween = ChronoUnit.YEARS.between(startDate.toLocalDate(), this.toLocalDate()) + return yearsBetween % everyX == 0L +} + +fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean { + repeatDays ?: return true // If no repeatDays specified, assume it matches + + return when (this.dayOfWeek) { + DayOfWeek.MONDAY -> repeatDays.m + DayOfWeek.TUESDAY -> repeatDays.t + DayOfWeek.WEDNESDAY -> repeatDays.w + DayOfWeek.THURSDAY -> repeatDays.th + DayOfWeek.FRIDAY -> repeatDays.f + DayOfWeek.SATURDAY -> repeatDays.s + DayOfWeek.SUNDAY -> repeatDays.su + else -> false + } +} + + +// Probably shouldn't be an extension function, but it's easier to test this way +fun ZonedDateTime.isReminderDue(reminderTime: ZonedDateTime, frequency: Frequency, everyX: Int, repeatDays: Days?, dayOfMonth: Int): Boolean { + // Check if the reminder is due based on the frequency and everyX + when (frequency) { + Frequency.DAILY -> { + if (!this.matchesDailyInterval(reminderTime, everyX)) return false + } + Frequency.WEEKLY -> { + if (!this.matchesWeeklyInterval(reminderTime, everyX)) return false + } + Frequency.MONTHLY -> { + if (!this.matchesMonthlyInterval(reminderTime, everyX, dayOfMonth)) return false + } + Frequency.YEARLY -> { + if (!this.matchesYearlyInterval(reminderTime, everyX)) return false + } + } + + // Check if the reminder is due based on the repeatDays + return this.matchesRepeatDays(repeatDays) +} + diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index bb500c2dd..c1ef83615 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -17,6 +17,8 @@ import com.habitrpg.common.habitica.helpers.ExceptionHandler import com.habitrpg.shared.habitica.HLogger import com.habitrpg.shared.habitica.LogLevel import com.habitrpg.shared.habitica.models.tasks.TaskType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -34,20 +36,41 @@ class TaskAlarmManager( private var authenticationHandler: AuthenticationHandler ) { private val am: AlarmManager? = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager + private val upcomingReminderOccurrencesToSchedule = 3 + + /** + * Schedules multiple alarms for each reminder associated with a given task. + * + * This method iterates through all reminders of a task and schedules multiple upcoming alarms for each. + * It determines the upcoming reminder times based on the reminder's configuration (like frequency, repeat days, etc.) + * and schedules an alarm for each of these times. + * + * For each reminder, it updates the reminder's time to each upcoming occurrence and then calls + * `setAlarmForRemindersItem` to handle the actual alarm scheduling. This ensures that each reminder + * is scheduled accurately according to its specified rules and times. + * + * This method is particularly useful for reminders that need to be repeated at regular intervals + * (e.g., daily, weekly) or on specific days, as it schedules multiple occurrences in advance. + * + * @param task The task for which the alarms are being set. Each reminder in the task's reminder list + * is processed to schedule the upcoming alarms. + */ private fun setAlarmsForTask(task: Task) { - task.reminders?.let { - for (reminder in it) { - var currentReminder = reminder - if (task.type == TaskType.DAILY) { - // Ensure that we set to the next available time - currentReminder = this.setTimeForDailyReminder(currentReminder, task) + CoroutineScope(Dispatchers.IO).launch { + task.reminders?.let { reminders -> + for (reminder in reminders) { + val upcomingReminders = task.getNextReminderOccurrences(reminder, upcomingReminderOccurrencesToSchedule) + upcomingReminders?.forEachIndexed { index, reminderNextOccurrenceTime -> + reminder?.time = reminderNextOccurrenceTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + setAlarmForRemindersItem(task, reminder, index) + } } - this.setAlarmForRemindersItem(task, currentReminder) } } } + private fun removeAlarmsForTask(task: Task) { task.reminders?.let { for (reminder in it) { @@ -81,26 +104,34 @@ class TaskAlarmManager( setAlarmsForTask(task) } - private fun setTimeForDailyReminder(remindersItem: RemindersItem?, task: Task): RemindersItem? { - val newTime = task.getNextReminderOccurrence(remindersItem, context) - remindersItem?.time = newTime?.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - - return remindersItem - } - /** - * If reminderItem time is before now, a new reminder will not be created until the reminder passes. - * The exception to this is if the task & reminder was newly created for the same time, - * in which the alarm will be created - - * which is indicated by first nextDue being null (As the alarm is created before the API returns nextDue times) + * Schedules an alarm for a given reminder associated with a task. + * + * This method takes a task and its associated reminder item to schedule an alarm. + * It first checks if the reminder time is valid and not in the past. If the reminder time + * is valid, it prepares an intent for the alarm, uniquely identified by combining the reminder's ID + * and its scheduled time. This unique identifier ensures that each reminder occurrence is distinctly handled. + * + * If an alarm with the same identifier already exists, it is cancelled and replaced with the new one. + * This ensures that reminders are always up to date with their latest scheduled times. + * + * The alarm is scheduled to trigger at the exact time specified in the reminder. Upon triggering, + * it will send a broadcast to `TaskReceiver`, which should handle the reminder notification. + * + * @param reminderItemTask The task associated with the reminder. + * @param remindersItem The reminder item containing details like ID and the time for the reminder. + * If this is null, the method returns immediately without scheduling an alarm. */ - private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?) { + private fun setAlarmForRemindersItem(reminderItemTask: Task, remindersItem: RemindersItem?, occurrenceIndex: Int) { if (remindersItem == null) return val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant() - if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) return + + if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) { + return + } val intent = Intent(context, TaskReceiver::class.java) @@ -108,7 +139,9 @@ class TaskAlarmManager( intent.putExtra(TASK_NAME_INTENT_KEY, reminderItemTask.text) intent.putExtra(TASK_ID_INTENT_KEY, reminderItemTask.id) - val intentId = remindersItem.id?.hashCode() ?: (0 and 0xfffffff) + // Create a unique identifier based on remindersItem.id and the occurrence index + val intentId = (remindersItem.id?.hashCode() ?: 0) + occurrenceIndex + // Cancel alarm if already exists val previousSender = PendingIntent.getBroadcast( context, @@ -128,7 +161,10 @@ class TaskAlarmManager( withImmutableFlag(PendingIntent.FLAG_CANCEL_CURRENT) ) - setAlarm(context, reminderZonedTime.toEpochMilli(), sender) + + CoroutineScope(Dispatchers.IO).launch { + setAlarm(context, reminderZonedTime.toEpochMilli(), sender) + } } private fun removeAlarmForRemindersItem(remindersItem: RemindersItem) { @@ -220,13 +256,16 @@ class TaskAlarmManager( } catch (ex: Exception) { when (ex) { is IllegalStateException, is SecurityException -> { - alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 60000, pendingIntent) + alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent) } - else -> throw ex + else -> { + throw ex + } + } } } else { - alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 60000, pendingIntent) + alarmManager?.setWindow(AlarmManager.RTC_WAKEUP, time, 600000, pendingIntent) } } } diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index 091688240..fa488da1f 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -1,12 +1,12 @@ package com.habitrpg.android.habitica.models.tasks -import android.content.Context import android.os.Parcel import android.os.Parcelable import android.text.Spanned import com.google.gson.annotations.SerializedName import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.extensions.dayOfWeekString +import com.habitrpg.android.habitica.extensions.isReminderDue +import com.habitrpg.android.habitica.extensions.matchesRepeatDays import com.habitrpg.android.habitica.extensions.parseToZonedDateTime import com.habitrpg.android.habitica.extensions.toZonedDateTime import com.habitrpg.android.habitica.models.BaseMainObject @@ -24,9 +24,9 @@ import io.realm.annotations.PrimaryKey import org.json.JSONArray import org.json.JSONException import java.time.LocalDate -import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit import java.util.Date open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { @@ -332,56 +332,97 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { fun checkIfDue(): Boolean = isDue == true /** - * When manually changing the alarm time, nextDue will provide the previously set upcoming alarm - - * however if the alarm was changed to a upcoming time today (and not before the current time) AND the previous alarm - * set for today already passed, we want to check if the alarm repeat/days includes today as well. - * If so - use today's date to schedule an alarm. If not, use nextDue. + * Calculates a list of the next upcoming reminder occurrences based on the reminder's configuration. * - * Alarms automatically rescheduled -> nextDue used. - * A alarm that already passed today and is rescheduled for a upcoming time today -> Schedule for today (instead of using nextDue) + * This method determines the next few occurrences of a reminder, taking into account its start date, + * frequency, repetition pattern, and other settings. It generates a list of ZonedDateTime instances, + * each representing a future point in time when the reminder is due. + * + * The method iterates starting from the reminder's start date and checks each day to see if it matches + * the criteria for triggering the reminder (like matching the specified days of the week, frequency, etc.). + * Once a matching date is found, it's added to the list with the specific time of the reminder. + * + * This is useful for scheduling reminders that need to occur multiple times in the future according + * to a specific pattern (e.g., every Monday and Wednesday at 10 AM). + * + * @param remindersItem The reminder item containing the configuration for calculating occurrences. + * @param occurrences The number of upcoming reminder occurrences to calculate. + * @return A list of ZonedDateTime instances, each representing an upcoming reminder occurrence. + * Returns null if the reminder item is null or essential information for calculation is missing. */ - fun getNextReminderOccurrence(remindersItem: RemindersItem?, context: Context): ZonedDateTime? { - if (remindersItem == null) { - return null + fun getNextReminderOccurrences(remindersItem: RemindersItem?, occurrences: Int): List? { + if (remindersItem == null) return null + + val reminderTime = remindersItem.time?.parseToZonedDateTime() ?: return null + val startDate = this.startDate?.toInstant()?.atZone(ZoneId.systemDefault()) ?: return null + val frequency = this.frequency ?: return null + val everyX = this.everyX ?: 1 + val repeatDays = this.repeat + + + // Determine the starting point: either the start date or the current date if start date is in the past + var currentDate = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) + if (startDate.isAfter(currentDate)) { + currentDate = startDate } - val reminderTime = remindersItem.time?.parseToZonedDateTime() - val zonedDateTimeNow = ZonedDateTime.now() + val occurrencesList = mutableListOf() - // Check if the reminder is scheduled to repeat today - val repeatingDays = repeat?.dayStrings(context) - val isScheduledForToday = repeatingDays?.find { day -> day == zonedDateTimeNow.dayOfWeekString() } - if (isScheduledForToday != null) { - val currentDateTime = LocalDateTime.now() - val updatedDateTime: LocalDateTime = LocalDateTime.of( - currentDateTime.year, - currentDateTime.month, - currentDateTime.dayOfMonth, - reminderTime?.hour ?: 0, - reminderTime?.minute ?: 0 - ) - return updatedDateTime.atZone(ZoneId.systemDefault()) + while (occurrencesList.size < occurrences) { + if (currentDate.isReminderDue(reminderTime, frequency, everyX, repeatDays, currentDate.dayOfMonth)) { + occurrencesList.add(currentDate.withHour(reminderTime.hour).withMinute(reminderTime.minute)) + } + + // Increment currentDate based on the frequency + currentDate = when (frequency) { + Frequency.DAILY -> currentDate.plusDays(everyX.toLong()) + Frequency.WEEKLY -> { + if (repeatDays?.hasAnyDaySelected() == true) { + // Find the next day of the week that matches the player's selection + var nextDueDate = currentDate + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + + // Calculate the number of weeks between the start date and the next due date + val weeksSinceStart = ChronoUnit.WEEKS.between(startDate.toLocalDate(), nextDueDate.toLocalDate()) + + // Check if the next due date falls in the correct week interval + if (weeksSinceStart % everyX != 0L) { + // If it doesn't, find the start of the next valid interval + val weeksToNextValidInterval = everyX - (weeksSinceStart % everyX) + nextDueDate = nextDueDate.plusWeeks(weeksToNextValidInterval) + + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + } + + // Ensure the next due date is in the future + val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) + if (nextDueDate.isBefore(now)) { + nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + } + + currentDate = nextDueDate + } else { + // If no specific days are selected, increment by the number of weeks + currentDate = currentDate.plusWeeks(everyX.toLong()) + } + currentDate + } + Frequency.MONTHLY -> currentDate.plusMonths(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth) + Frequency.YEARLY -> currentDate.plusYears(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth).withMonth(startDate.monthValue) + } } - // If the reminder is not scheduled to repeat today, use the first upcoming nextDue date - val today = LocalDate.now() - // Filter out the dates that are in the past - val futureNextDues = nextDue?.filter { nextDueDate -> nextDueDate.toInstant().atZone(ZonedDateTime.now().zone).toLocalDate().isAfter(today) } - val earliestFutureDate = futureNextDues?.minByOrNull { it } - if (earliestFutureDate != null) { - return earliestFutureDate.toInstant() - .atZone(ZonedDateTime.now().zone) - .withHour(reminderTime?.hour ?: 0) - .withMinute(reminderTime?.minute ?: 0) - } - - - // If there are no upcoming nextDue dates, use the first upcoming reminder time - return reminderTime + return occurrencesList } - fun parseMarkdown() { parsedText = MarkdownParser.parseMarkdown(text) parsedNotes = MarkdownParser.parseMarkdown(notes) @@ -585,6 +626,10 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { } } + private fun Days.hasAnyDaySelected(): Boolean { + return m || t || w || th || f || s || su + } + fun getDaysOfMonth(): List? { if (daysOfMonth == null) { val daysOfMonth = mutableListOf() From d904c3e24dd34d4e70889a8de1cd66c8e8db08fb Mon Sep 17 00:00:00 2001 From: Hafiz Date: Thu, 30 Nov 2023 23:15:31 -0500 Subject: [PATCH 4/9] Fixes for reminders (Week & Year) --- .../android/habitica/models/tasks/Task.kt | 117 ++++++++++-------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index fa488da1f..30fac0383 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -5,7 +5,6 @@ import android.os.Parcelable import android.text.Spanned import com.google.gson.annotations.SerializedName import com.habitrpg.android.habitica.R -import com.habitrpg.android.habitica.extensions.isReminderDue import com.habitrpg.android.habitica.extensions.matchesRepeatDays import com.habitrpg.android.habitica.extensions.parseToZonedDateTime import com.habitrpg.android.habitica.extensions.toZonedDateTime @@ -354,69 +353,85 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { if (remindersItem == null) return null val reminderTime = remindersItem.time?.parseToZonedDateTime() ?: return null - val startDate = this.startDate?.toInstant()?.atZone(ZoneId.systemDefault()) ?: return null + var startDate = this.startDate?.toInstant()?.atZone(ZoneId.systemDefault()) ?: return null val frequency = this.frequency ?: return null val everyX = this.everyX ?: 1 val repeatDays = this.repeat + startDate = startDate.withHour(reminderTime.hour).withMinute(reminderTime.minute) - - // Determine the starting point: either the start date or the current date if start date is in the past - var currentDate = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) - if (startDate.isAfter(currentDate)) { - currentDate = startDate - } + var dateTimeOccurenceToSchedule = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) val occurrencesList = mutableListOf() while (occurrencesList.size < occurrences) { - if (currentDate.isReminderDue(reminderTime, frequency, everyX, repeatDays, currentDate.dayOfMonth)) { - occurrencesList.add(currentDate.withHour(reminderTime.hour).withMinute(reminderTime.minute)) - } - // Increment currentDate based on the frequency - currentDate = when (frequency) { - Frequency.DAILY -> currentDate.plusDays(everyX.toLong()) - Frequency.WEEKLY -> { - if (repeatDays?.hasAnyDaySelected() == true) { - // Find the next day of the week that matches the player's selection - var nextDueDate = currentDate - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - - // Calculate the number of weeks between the start date and the next due date - val weeksSinceStart = ChronoUnit.WEEKS.between(startDate.toLocalDate(), nextDueDate.toLocalDate()) - - // Check if the next due date falls in the correct week interval - if (weeksSinceStart % everyX != 0L) { - // If it doesn't, find the start of the next valid interval - val weeksToNextValidInterval = everyX - (weeksSinceStart % everyX) - nextDueDate = nextDueDate.plusWeeks(weeksToNextValidInterval) - - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - } - - // Ensure the next due date is in the future - val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) - if (nextDueDate.isBefore(now)) { - nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - } - - currentDate = nextDueDate + dateTimeOccurenceToSchedule = when (frequency) { + Frequency.DAILY -> { + dateTimeOccurenceToSchedule = if (dateTimeOccurenceToSchedule.isBefore(startDate)) { + startDate } else { - // If no specific days are selected, increment by the number of weeks - currentDate = currentDate.plusWeeks(everyX.toLong()) + dateTimeOccurenceToSchedule.plusDays(everyX.toLong()).withHour(reminderTime.hour).withMinute(reminderTime.minute) } - currentDate + dateTimeOccurenceToSchedule + } + Frequency.WEEKLY -> { + if (dateTimeOccurenceToSchedule.isBefore(startDate)) { + dateTimeOccurenceToSchedule = startDate + } else { + // Check if all days are selected + if (repeatDays?.hasAnyDaySelected() == true) { + // Simply increment by one day + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(1) + } else { + // Logic for specific days selected + var nextDueDate = dateTimeOccurenceToSchedule + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + + val weeksSinceStart = ChronoUnit.WEEKS.between(startDate.toLocalDate(), nextDueDate.toLocalDate()) + if (weeksSinceStart % everyX != 0L) { + val weeksToNextValidInterval = everyX - (weeksSinceStart % everyX) + nextDueDate = nextDueDate.plusWeeks(weeksToNextValidInterval) + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + } + + val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) + if (nextDueDate.isBefore(now)) { + nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1) + } + } + + dateTimeOccurenceToSchedule = nextDueDate + } + } + dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.withHour(reminderTime.hour).withMinute(reminderTime.minute) + dateTimeOccurenceToSchedule + } + + Frequency.MONTHLY -> { + dateTimeOccurenceToSchedule = if (dateTimeOccurenceToSchedule.isBefore(startDate)) { + startDate + } else { + dateTimeOccurenceToSchedule.plusMonths(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth).withHour(reminderTime.hour).withMinute(reminderTime.minute) + } + dateTimeOccurenceToSchedule + } + Frequency.YEARLY -> { + dateTimeOccurenceToSchedule = if (dateTimeOccurenceToSchedule.isBefore(startDate)) { + startDate + } else { + dateTimeOccurenceToSchedule.plusYears(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth).withMonth(startDate.monthValue).withHour(reminderTime.hour).withMinute(reminderTime.minute) + } + dateTimeOccurenceToSchedule } - Frequency.MONTHLY -> currentDate.plusMonths(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth) - Frequency.YEARLY -> currentDate.plusYears(everyX.toLong()).withDayOfMonth(startDate.dayOfMonth).withMonth(startDate.monthValue) } + + occurrencesList.add(dateTimeOccurenceToSchedule) } From 7aac6eb6ac5d768614d4684bbbbb15fff12d0722 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Thu, 30 Nov 2023 23:29:56 -0500 Subject: [PATCH 5/9] Remove unused extensions --- .../extensions/ZonedDateExtensions.kt | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt index 9c4d19f64..a525ee706 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/extensions/ZonedDateExtensions.kt @@ -47,26 +47,6 @@ fun formatter(): DateTimeFormatter = .toFormatter() -fun ZonedDateTime.matchesDailyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { - val daysBetween = ChronoUnit.DAYS.between(startDate.toLocalDate(), this.toLocalDate()) - return daysBetween % everyX == 0L -} - -fun ZonedDateTime.matchesWeeklyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { - val weeksBetween = ChronoUnit.WEEKS.between(startDate.toLocalDate(), this.toLocalDate()) - return weeksBetween % everyX == 0L -} - -fun ZonedDateTime.matchesMonthlyInterval(startDate: ZonedDateTime, everyX: Int, dayOfMonth: Int): Boolean { - val monthsBetween = ChronoUnit.MONTHS.between(startDate.toLocalDate(), this.toLocalDate()) - return this.dayOfMonth == dayOfMonth && monthsBetween % everyX == 0L -} - -fun ZonedDateTime.matchesYearlyInterval(startDate: ZonedDateTime, everyX: Int): Boolean { - val yearsBetween = ChronoUnit.YEARS.between(startDate.toLocalDate(), this.toLocalDate()) - return yearsBetween % everyX == 0L -} - fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean { repeatDays ?: return true // If no repeatDays specified, assume it matches @@ -83,25 +63,5 @@ fun ZonedDateTime.matchesRepeatDays(repeatDays: Days?): Boolean { } -// Probably shouldn't be an extension function, but it's easier to test this way -fun ZonedDateTime.isReminderDue(reminderTime: ZonedDateTime, frequency: Frequency, everyX: Int, repeatDays: Days?, dayOfMonth: Int): Boolean { - // Check if the reminder is due based on the frequency and everyX - when (frequency) { - Frequency.DAILY -> { - if (!this.matchesDailyInterval(reminderTime, everyX)) return false - } - Frequency.WEEKLY -> { - if (!this.matchesWeeklyInterval(reminderTime, everyX)) return false - } - Frequency.MONTHLY -> { - if (!this.matchesMonthlyInterval(reminderTime, everyX, dayOfMonth)) return false - } - Frequency.YEARLY -> { - if (!this.matchesYearlyInterval(reminderTime, everyX)) return false - } - } - // Check if the reminder is due based on the repeatDays - return this.matchesRepeatDays(repeatDays) -} From 503271c440029129f5c860c888687ea9ac897194 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Mon, 1 Jan 2024 10:33:22 -0500 Subject: [PATCH 6/9] Refactor weekly reminder scheduling logic for specific days intervals Refactor weekly reminder scheduling logic to handle specific days and intervals correctly --- .../habitica/helpers/TaskAlarmManager.kt | 3 +- .../android/habitica/models/tasks/Task.kt | 56 +++++++++---------- 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index c1ef83615..2da0d9ac0 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -5,6 +5,7 @@ import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import androidx.preference.PreferenceManager import com.habitrpg.android.habitica.data.TaskRepository import com.habitrpg.android.habitica.extensions.withImmutableFlag @@ -128,7 +129,6 @@ class TaskAlarmManager( val now = ZonedDateTime.now().withZoneSameLocal(ZoneId.systemDefault())?.toInstant() val reminderZonedTime = remindersItem.getLocalZonedDateTimeInstant() - if (reminderZonedTime == null || reminderZonedTime.isBefore(now)) { return } @@ -253,6 +253,7 @@ class TaskAlarmManager( // For SDK >= Android 12, allows batching of reminders try { alarmManager?.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, time, pendingIntent) + Log.d("TaskAlarmManager", "setAlarm: Scheduling for $time using setAndAllowWhileIdle") } catch (ex: Exception) { when (ex) { is IllegalStateException, is SecurityException -> { diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index 30fac0383..62f14f928 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -375,40 +375,38 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { dateTimeOccurenceToSchedule } Frequency.WEEKLY -> { + // Set to start date if current date is earlier if (dateTimeOccurenceToSchedule.isBefore(startDate)) { dateTimeOccurenceToSchedule = startDate } else { - // Check if all days are selected - if (repeatDays?.hasAnyDaySelected() == true) { - // Simply increment by one day - dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.plusDays(1) - } else { - // Logic for specific days selected - var nextDueDate = dateTimeOccurenceToSchedule - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - - val weeksSinceStart = ChronoUnit.WEEKS.between(startDate.toLocalDate(), nextDueDate.toLocalDate()) - if (weeksSinceStart % everyX != 0L) { - val weeksToNextValidInterval = everyX - (weeksSinceStart % everyX) - nextDueDate = nextDueDate.plusWeeks(weeksToNextValidInterval) - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - } - - val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) - if (nextDueDate.isBefore(now)) { - nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1) - } - } - - dateTimeOccurenceToSchedule = nextDueDate + var nextDueDate = dateTimeOccurenceToSchedule.withHour(reminderTime.hour).withMinute(reminderTime.minute) + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1).withHour(reminderTime.hour).withMinute(reminderTime.minute) } + // Calculate weeks since start and adjust for the correct interval + val weeksSinceStart = ChronoUnit.WEEKS.between(startDate.toLocalDate(), nextDueDate.toLocalDate()) + if (weeksSinceStart % everyX != 0L) { + val weeksToNextValidInterval = everyX - (weeksSinceStart % everyX) + nextDueDate = nextDueDate.plusWeeks(weeksToNextValidInterval) + // Find the exact next due day within the valid interval + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1).withHour(reminderTime.hour).withMinute(reminderTime.minute) + } + } + + // Ensure the next due date is in the future + val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) + if (nextDueDate.isBefore(now)) { + nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) + // Find the next due day in the future + while (!nextDueDate.matchesRepeatDays(repeatDays)) { + nextDueDate = nextDueDate.plusDays(1).withHour(reminderTime.hour).withMinute(reminderTime.minute) + } + } + + dateTimeOccurenceToSchedule = nextDueDate } + // Set time to the reminder time dateTimeOccurenceToSchedule = dateTimeOccurenceToSchedule.withHour(reminderTime.hour).withMinute(reminderTime.minute) dateTimeOccurenceToSchedule } From 5b1a6dd2e966b8ed35c09664f4ad059af184e82e Mon Sep 17 00:00:00 2001 From: Hafiz Date: Mon, 1 Jan 2024 11:01:45 -0500 Subject: [PATCH 7/9] Remove alarms/reminders on task deletion --- .../android/habitica/helpers/TaskAlarmManager.kt | 15 +++++++++------ .../habitica/ui/activities/TaskFormActivity.kt | 2 ++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt index 2da0d9ac0..6bc16793e 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/helpers/TaskAlarmManager.kt @@ -72,10 +72,13 @@ class TaskAlarmManager( } - private fun removeAlarmsForTask(task: Task) { - task.reminders?.let { - for (reminder in it) { - this.removeAlarmForRemindersItem(reminder) + fun removeAlarmsForTask(task: Task) { + CoroutineScope(Dispatchers.IO).launch { + task.reminders?.let { reminders -> + // Remove not only the immediate reminder, but also the next however many (upcomingReminderOccurrencesToSchedule) reminders + reminders.forEachIndexed { index, reminder -> + removeAlarmForRemindersItem(reminder, index) + } } } } @@ -167,10 +170,10 @@ class TaskAlarmManager( } } - private fun removeAlarmForRemindersItem(remindersItem: RemindersItem) { + private fun removeAlarmForRemindersItem(remindersItem: RemindersItem, occurrenceIndex: Int? = null) { val intent = Intent(context, TaskReceiver::class.java) intent.action = remindersItem.id - val intentId = remindersItem.id?.hashCode() ?: (0 and 0xfffffff) + val intentId = if (occurrenceIndex != null) (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) + occurrenceIndex else (remindersItem.id?.hashCode() ?: (0 and 0xfffffff)) val sender = PendingIntent.getBroadcast( context, intentId, diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt index 45dfb5e09..fb0daf80c 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/ui/activities/TaskFormActivity.kt @@ -835,6 +835,8 @@ class TaskFormActivity : BaseActivity() { task?.id?.let { lifecycleScope.launch(Dispatchers.Main) { taskRepository.deleteTask(it) + val taskCopy = task + taskCopy?.let { taskAlarmManager.removeAlarmsForTask(it) } } } finish() From 779110c618711619a84a6988c48607484f36f2c9 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Mon, 1 Jan 2024 11:24:13 -0500 Subject: [PATCH 8/9] Handle weekly same day reminder that already passed --- .../android/habitica/models/tasks/Task.kt | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index 62f14f928..aee2aee59 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -353,6 +353,7 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { if (remindersItem == null) return null val reminderTime = remindersItem.time?.parseToZonedDateTime() ?: return null + val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) var startDate = this.startDate?.toInstant()?.atZone(ZoneId.systemDefault()) ?: return null val frequency = this.frequency ?: return null val everyX = this.everyX ?: 1 @@ -380,6 +381,16 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { dateTimeOccurenceToSchedule = startDate } else { var nextDueDate = dateTimeOccurenceToSchedule.withHour(reminderTime.hour).withMinute(reminderTime.minute) + // If the next due date already happened for today, increment it by one day. Otherwise, it will be scheduled for today. + if (nextDueDate.isBefore(now) && occurrencesList.size == 0) { + nextDueDate = nextDueDate.plusDays(1) + } + + // If the reminder being scheduled is not the first iteration of the reminder, increment it by one day + if (occurrencesList.size > 0) { + nextDueDate = nextDueDate.plusDays(1) + } + while (!nextDueDate.matchesRepeatDays(repeatDays)) { nextDueDate = nextDueDate.plusDays(1).withHour(reminderTime.hour).withMinute(reminderTime.minute) } @@ -394,16 +405,6 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { } } - // Ensure the next due date is in the future - val now = ZonedDateTime.now().withZoneSameInstant(ZoneId.systemDefault()) - if (nextDueDate.isBefore(now)) { - nextDueDate = nextDueDate.plusWeeks(everyX.toLong()) - // Find the next due day in the future - while (!nextDueDate.matchesRepeatDays(repeatDays)) { - nextDueDate = nextDueDate.plusDays(1).withHour(reminderTime.hour).withMinute(reminderTime.minute) - } - } - dateTimeOccurenceToSchedule = nextDueDate } // Set time to the reminder time From 25e1682e68d48bd58e3a258c8df7e31a3a32da93 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Tue, 2 Jan 2024 09:17:26 -0500 Subject: [PATCH 9/9] Handle if no days are selected for a weekly --- .../java/com/habitrpg/android/habitica/models/tasks/Task.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt index aee2aee59..2c61252e1 100644 --- a/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt +++ b/Habitica/src/main/java/com/habitrpg/android/habitica/models/tasks/Task.kt @@ -379,7 +379,8 @@ open class Task : RealmObject, BaseMainObject, Parcelable, BaseTask { // Set to start date if current date is earlier if (dateTimeOccurenceToSchedule.isBefore(startDate)) { dateTimeOccurenceToSchedule = startDate - } else { + } else if (repeatDays?.hasAnyDaySelected() == true) { + var nextDueDate = dateTimeOccurenceToSchedule.withHour(reminderTime.hour).withMinute(reminderTime.minute) // If the next due date already happened for today, increment it by one day. Otherwise, it will be scheduled for today. if (nextDueDate.isBefore(now) && occurrencesList.size == 0) {