mirror of https://github.com/tasks/tasks
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.
279 lines
11 KiB
Kotlin
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
|
|
}
|
|
} |