You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt

279 lines
11 KiB
Kotlin

/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import org.tasks.LocalBroadcastManager
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime
import timber.log.Timber
import java.text.ParseException
import java.util.*
import javax.inject.Inject
class RepeatTaskHelper @Inject constructor(
private val gcalHelper: GCalHelper,
private val alarmService: AlarmService,
private val taskDao: TaskDao,
private val localBroadcastManager: LocalBroadcastManager,
private val taskCompleter: TaskCompleter,
) {
suspend fun handleRepeat(task: Task) {
val recurrence = task.recurrence
if (recurrence.isNullOrBlank()) {
return
}
val repeatAfterCompletion = task.repeatAfterCompletion()
val newDueDate: Long
val rrule: Recur
val count: Int
try {
rrule = initRRule(recurrence)
count = rrule.count
if (count == 1) {
broadcastCompletion(task)
return
}
newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion)
if (newDueDate == -1L) {
return
}
} catch (e: ParseException) {
Timber.e(e)
return
}
val oldDueDate = task.dueDate
val repeatUntil = task.repeatUntil
if (repeatFinished(newDueDate, repeatUntil)) {
broadcastCompletion(task)
return
}
if (count > 1) {
rrule.count = count - 1
task.setRecurrence(rrule)
}
task.reminderLast = 0L
task.completionDate = 0L
task.setDueDateAdjustingHideUntil(newDueDate)
gcalHelper.rescheduleRepeatingTask(task)
taskDao.save(task)
val previousDueDate =
oldDueDate
.takeIf { it > 0 }
?: (newDueDate - (computeNextDueDate(task, recurrence, repeatAfterCompletion) - newDueDate))
rescheduleAlarms(task.id, previousDueDate, newDueDate)
taskCompleter.setComplete(task, false)
broadcastCompletion(task, previousDueDate)
}
private fun broadcastCompletion(task: Task, oldDueDate: Long = 0L) {
if (!task.isSuppressRefresh()) {
localBroadcastManager.broadcastTaskCompleted(task.id, oldDueDate)
}
}
suspend fun undoRepeat(task: Task, oldDueDate: Long) {
task.completionDate = 0L
try {
val recur = newRecur(task.recurrence!!)
val count = recur.count
if (count > 0) {
recur.count = count + 1
}
task.setRecurrence(recur)
val newDueDate = task.dueDate
task.setDueDateAdjustingHideUntil(
if (oldDueDate > 0) {
oldDueDate
} else {
newDueDate - (computeNextDueDate(task, task.recurrence!!, false) - newDueDate)
}
)
rescheduleAlarms(task.id, newDueDate, task.dueDate)
} catch (e: ParseException) {
Timber.e(e)
}
taskDao.save(task)
}
private suspend fun rescheduleAlarms(taskId: Long, oldDueDate: Long, newDueDate: Long) {
if (oldDueDate <= 0 || newDueDate <= 0) {
return
}
alarmService.getAlarms(taskId)
.filter { it.type != TYPE_SNOOZE }
.onEach {
if (it.type == Alarm.TYPE_DATE_TIME) {
it.time += newDueDate - oldDueDate
}
}
.let { alarmService.synchronizeAlarms(taskId, it.toMutableSet()) }
}
companion object {
private val weekdayCompare = Comparator { object1: WeekDay, object2: WeekDay -> WeekDay.getCalendarDay(object1) - WeekDay.getCalendarDay(object2) }
private fun repeatFinished(newDueDate: Long, repeatUntil: Long): Boolean {
return repeatUntil > 0 && newDateTime(newDueDate).startOfDay().millis > repeatUntil
}
/** Compute next due date */
@Throws(ParseException::class)
fun computeNextDueDate(task: Task, recurrence: String, repeatAfterCompletion: Boolean): Long {
val rrule = initRRule(recurrence)
// initialize startDateAsDV
val original = setUpStartDate(task, repeatAfterCompletion, rrule.frequency)
val startDateAsDV = setUpStartDateAsDV(task, original)
return if (rrule.frequency == Recur.Frequency.HOURLY || rrule.frequency == Recur.Frequency.MINUTELY) {
handleSubdayRepeat(original, rrule)
} else if (rrule.frequency == Recur.Frequency.WEEKLY && rrule.dayList.isNotEmpty() && repeatAfterCompletion) {
handleWeeklyRepeatAfterComplete(rrule, original, task.hasDueTime())
} else if (rrule.frequency == Recur.Frequency.MONTHLY && rrule.dayList.isEmpty()) {
handleMonthlyRepeat(original, startDateAsDV, task.hasDueTime(), rrule)
} else {
invokeRecurrence(rrule, original, startDateAsDV)
}
}
private fun handleWeeklyRepeatAfterComplete(
recur: Recur, original: DateTime, hasDueTime: Boolean): Long {
val byDay = recur.dayList
var newDate = original.millis
newDate += DateUtilities.ONE_WEEK * (recur.interval - 1)
var date = DateTime(newDate)
Collections.sort(byDay, weekdayCompare)
val next = findNextWeekday(byDay, date)
do {
date = date.plusDays(1)
} while (date.dayOfWeek != WeekDay.getCalendarDay(next))
val time = date.millis
return if (hasDueTime) {
createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time)
} else {
createDueDate(Task.URGENCY_SPECIFIC_DAY, time)
}
}
private fun handleMonthlyRepeat(
original: DateTime, startDateAsDV: Date, hasDueTime: Boolean, recur: Recur): Long {
return if (original.isLastDayOfMonth) {
val interval = recur.interval.coerceAtLeast(1)
val newDateTime = original.plusMonths(interval)
val time = newDateTime.withDayOfMonth(newDateTime.numberOfDaysInMonth).millis
if (hasDueTime) {
createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time)
} else {
createDueDate(Task.URGENCY_SPECIFIC_DAY, time)
}
} else {
invokeRecurrence(recur, original, startDateAsDV)
}
}
private fun findNextWeekday(byDay: List<WeekDay>, date: DateTime): WeekDay {
val next = byDay[0]
for (weekday in byDay) {
if (WeekDay.getCalendarDay(weekday) > date.dayOfWeek) {
return weekday
}
}
return next
}
private fun invokeRecurrence(recur: Recur, original: DateTime, startDateAsDV: Date): Long {
return recur.getNextDate(startDateAsDV, startDateAsDV)
?.let { buildNewDueDate(original, it) }
?: throw IllegalStateException("recur=$recur original=$original startDateAsDv=$startDateAsDV")
}
/** Compute long due date from DateValue */
private fun buildNewDueDate(original: DateTime, nextDate: Date): Long {
val newDueDate: Long
if (nextDate is net.fortuna.ical4j.model.DateTime) {
var date = DateTime.from(nextDate)
// time may be inaccurate due to DST, force time to be same
date = date.withHourOfDay(original.hourOfDay).withMinuteOfHour(original.minuteOfHour)
newDueDate = createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.millis)
} else {
newDueDate = createDueDate(
Task.URGENCY_SPECIFIC_DAY,
DateTime.from(nextDate).millis)
}
return newDueDate
}
/** Initialize RRule instance */
@Throws(ParseException::class)
private fun initRRule(recurrence: String): Recur {
val rrule = newRecur(recurrence)
// handle the iCalendar "byDay" field differently depending on if
// we are weekly or otherwise
if (rrule.frequency != Recur.Frequency.WEEKLY && rrule.frequency != Recur.Frequency.MONTHLY) {
rrule.dayList.clear()
}
return rrule
}
/** Set up repeat start date */
private fun setUpStartDate(
task: Task, repeatAfterCompletion: Boolean, frequency: Recur.Frequency): DateTime {
return if (repeatAfterCompletion) {
var startDate = if (task.isCompleted) newDateTime(task.completionDate) else newDateTime()
if (task.hasDueTime() && frequency != Recur.Frequency.HOURLY && frequency != Recur.Frequency.MINUTELY) {
val dueDate = newDateTime(task.dueDate)
startDate = startDate
.withHourOfDay(dueDate.hourOfDay)
.withMinuteOfHour(dueDate.minuteOfHour)
.withSecondOfMinute(dueDate.secondOfMinute)
}
startDate
} else {
if (task.hasDueDate()) newDateTime(task.dueDate) else newDateTime()
}
}
private fun setUpStartDateAsDV(task: Task, startDate: DateTime): Date {
return if (task.hasDueTime()) {
startDate.toDateTime()
} else {
startDate.toDate()
}
}
private fun handleSubdayRepeat(startDate: DateTime, recur: Recur): Long {
val millis: Long = when (recur.frequency) {
Recur.Frequency.HOURLY -> DateUtilities.ONE_HOUR
Recur.Frequency.MINUTELY -> DateUtilities.ONE_MINUTE
else -> throw RuntimeException(
"Error handing subday repeat: " + recur.frequency) // $NON-NLS-1$
}
val newDueDate = startDate.millis + millis * recur.interval
return createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDueDate)
}
private val Task.repeatUntil: Long
get() = recurrence
?.takeIf { it.isNotBlank() }
?.let { newRecur(it) }
?.until
?.let { DateTime.from(it) }
?.millis
?: 0L
}
}