Convert repeat task helper to coroutines

pull/1055/head
Alex Baker 4 years ago
parent 0661be1769
commit 00bfd053c6

@ -222,6 +222,7 @@ dependencies {
androidTestImplementation("androidx.annotation:annotation:1.1.0")
testImplementation("junit:junit:4.13")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.8")
testImplementation("com.natpryce:make-it-easy:${Versions.make_it_easy}")
testImplementation("androidx.test:core:${Versions.androidx_test}")
testImplementation("org.mockito:mockito-core:${Versions.mockito}")

@ -1,298 +0,0 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats;
import static org.tasks.date.DateTimeUtils.newDate;
import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.date.DateTimeUtils.newDateUtc;
import com.google.ical.iter.RecurrenceIterator;
import com.google.ical.iter.RecurrenceIteratorFactory;
import com.google.ical.values.DateTimeValueImpl;
import com.google.ical.values.DateValue;
import com.google.ical.values.DateValueImpl;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.WeekdayNum;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.dao.TaskDaoBlocking;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.gcal.GCalHelper;
import java.text.ParseException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.TimeZone;
import javax.inject.Inject;
import org.tasks.LocalBroadcastManager;
import org.tasks.time.DateTime;
import timber.log.Timber;
public class RepeatTaskHelper {
private static final Comparator<WeekdayNum> weekdayCompare =
(object1, object2) -> object1.wday.javaDayNum - object2.wday.javaDayNum;
private final GCalHelper gcalHelper;
private final TaskDaoBlocking taskDao;
private final LocalBroadcastManager localBroadcastManager;
private final AlarmService alarmService;
@Inject
public RepeatTaskHelper(
GCalHelper gcalHelper,
AlarmService alarmService,
TaskDaoBlocking taskDao,
LocalBroadcastManager localBroadcastManager) {
this.gcalHelper = gcalHelper;
this.taskDao = taskDao;
this.localBroadcastManager = localBroadcastManager;
this.alarmService = alarmService;
}
private static boolean repeatFinished(long newDueDate, long repeatUntil) {
return repeatUntil > 0
&& newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay());
}
/** Compute next due date */
static long computeNextDueDate(Task task, String recurrence, boolean repeatAfterCompletion)
throws ParseException {
RRule rrule = initRRule(recurrence);
// initialize startDateAsDV
DateTime original = setUpStartDate(task, repeatAfterCompletion, rrule.getFreq());
DateValue startDateAsDV = setUpStartDateAsDV(task, original);
if (rrule.getFreq() == Frequency.HOURLY || rrule.getFreq() == Frequency.MINUTELY) {
return handleSubdayRepeat(original, rrule);
} else if (rrule.getFreq() == Frequency.WEEKLY
&& rrule.getByDay().size() > 0
&& repeatAfterCompletion) {
return handleWeeklyRepeatAfterComplete(rrule, original, task.hasDueTime());
} else if (rrule.getFreq() == Frequency.MONTHLY && rrule.getByDay().isEmpty()) {
return handleMonthlyRepeat(original, startDateAsDV, task.hasDueTime(), rrule);
} else {
return invokeRecurrence(rrule, original, startDateAsDV);
}
}
private static long handleWeeklyRepeatAfterComplete(
RRule rrule, DateTime original, boolean hasDueTime) {
List<WeekdayNum> byDay = rrule.getByDay();
long newDate = original.getMillis();
newDate += DateUtilities.ONE_WEEK * (rrule.getInterval() - 1);
DateTime date = new DateTime(newDate);
Collections.sort(byDay, weekdayCompare);
WeekdayNum next = findNextWeekday(byDay, date);
do {
date = date.plusDays(1);
} while (date.getDayOfWeek() != next.wday.javaDayNum);
long time = date.getMillis();
if (hasDueTime) {
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time);
} else {
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, time);
}
}
private static long handleMonthlyRepeat(
DateTime original, DateValue startDateAsDV, boolean hasDueTime, RRule rrule) {
if (original.isLastDayOfMonth()) {
int interval = rrule.getInterval();
DateTime newDateTime = original.plusMonths(interval);
long time = newDateTime.withDayOfMonth(newDateTime.getNumberOfDaysInMonth()).getMillis();
if (hasDueTime) {
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, time);
} else {
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, time);
}
} else {
return invokeRecurrence(rrule, original, startDateAsDV);
}
}
private static WeekdayNum findNextWeekday(List<WeekdayNum> byDay, DateTime date) {
WeekdayNum next = byDay.get(0);
for (WeekdayNum weekday : byDay) {
if (weekday.wday.javaDayNum > date.getDayOfWeek()) {
return weekday;
}
}
return next;
}
private static long invokeRecurrence(RRule rrule, DateTime original, DateValue startDateAsDV) {
long newDueDate = -1;
RecurrenceIterator iterator =
RecurrenceIteratorFactory.createRecurrenceIterator(
rrule, startDateAsDV, TimeZone.getDefault());
DateValue nextDate;
for (int i = 0; i < 10; i++) { // ten tries then we give up
if (!iterator.hasNext()) {
return -1;
}
nextDate = iterator.next();
if (nextDate.compareTo(startDateAsDV) == 0) {
continue;
}
newDueDate = buildNewDueDate(original, nextDate);
// detect if we finished
if (newDueDate > original.getMillis()) {
break;
}
}
return newDueDate;
}
/** Compute long due date from DateValue */
private static long buildNewDueDate(DateTime original, DateValue nextDate) {
long newDueDate;
if (nextDate instanceof DateTimeValueImpl) {
DateTimeValueImpl newDateTime = (DateTimeValueImpl) nextDate;
DateTime date =
newDateUtc(
newDateTime.year(),
newDateTime.month(),
newDateTime.day(),
newDateTime.hour(),
newDateTime.minute(),
newDateTime.second())
.toLocal();
// time may be inaccurate due to DST, force time to be same
date =
date.withHourOfDay(original.getHourOfDay()).withMinuteOfHour(original.getMinuteOfHour());
newDueDate = Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.getMillis());
} else {
newDueDate =
Task.createDueDate(
Task.URGENCY_SPECIFIC_DAY,
newDate(nextDate.year(), nextDate.month(), nextDate.day()).getMillis());
}
return newDueDate;
}
/** Initialize RRule instance */
private static RRule initRRule(String recurrence) throws ParseException {
RRule rrule = new RRule(recurrence);
// handle the iCalendar "byDay" field differently depending on if
// we are weekly or otherwise
if (rrule.getFreq() != Frequency.WEEKLY && rrule.getFreq() != Frequency.MONTHLY) {
rrule.setByDay(Collections.emptyList());
}
return rrule;
}
/** Set up repeat start date */
private static DateTime setUpStartDate(
Task task, boolean repeatAfterCompletion, Frequency frequency) {
if (repeatAfterCompletion) {
DateTime startDate =
task.isCompleted() ? newDateTime(task.getCompletionDate()) : newDateTime();
if (task.hasDueTime() && frequency != Frequency.HOURLY && frequency != Frequency.MINUTELY) {
DateTime dueDate = newDateTime(task.getDueDate());
startDate =
startDate
.withHourOfDay(dueDate.getHourOfDay())
.withMinuteOfHour(dueDate.getMinuteOfHour())
.withSecondOfMinute(dueDate.getSecondOfMinute());
}
return startDate;
} else {
return task.hasDueDate() ? newDateTime(task.getDueDate()) : newDateTime();
}
}
private static DateValue setUpStartDateAsDV(Task task, DateTime startDate) {
if (task.hasDueTime()) {
return new DateTimeValueImpl(
startDate.getYear(),
startDate.getMonthOfYear(),
startDate.getDayOfMonth(),
startDate.getHourOfDay(),
startDate.getMinuteOfHour(),
startDate.getSecondOfMinute());
} else {
return new DateValueImpl(
startDate.getYear(), startDate.getMonthOfYear(), startDate.getDayOfMonth());
}
}
private static long handleSubdayRepeat(DateTime startDate, RRule rrule) {
long millis;
switch (rrule.getFreq()) {
case HOURLY:
millis = DateUtilities.ONE_HOUR;
break;
case MINUTELY:
millis = DateUtilities.ONE_MINUTE;
break;
default:
throw new RuntimeException(
"Error handing subday repeat: " + rrule.getFreq()); // $NON-NLS-1$
}
long newDueDate = startDate.getMillis() + millis * rrule.getInterval();
return Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDueDate);
}
public void handleRepeat(Task task) {
String recurrence = task.sanitizedRecurrence();
boolean repeatAfterCompletion = task.repeatAfterCompletion();
if (recurrence != null && recurrence.length() > 0) {
long newDueDate;
RRule rrule;
try {
rrule = initRRule(task.getRecurrenceWithoutFrom());
newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion);
if (newDueDate == -1) {
return;
}
} catch (ParseException e) {
Timber.e(e);
return;
}
long oldDueDate = task.getDueDate();
long repeatUntil = task.getRepeatUntil();
if (repeatFinished(newDueDate, repeatUntil)) {
return;
}
int count = rrule.getCount();
if (count == 1) {
return;
}
if (count > 1) {
rrule.setCount(count - 1);
task.setRecurrence(rrule, repeatAfterCompletion);
}
task.setReminderSnooze(0L);
task.setCompletionDate(0L);
task.setDueDateAdjustingHideUntil(newDueDate);
gcalHelper.rescheduleRepeatingTask(task);
taskDao.save(task);
alarmService.rescheduleAlarms(task.getId(), oldDueDate, newDueDate);
localBroadcastManager.broadcastRepeat(task.getId(), oldDueDate, newDueDate);
}
}
}

@ -0,0 +1,244 @@
/*
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.repeats
import com.google.ical.iter.RecurrenceIteratorFactory
import com.google.ical.values.*
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 org.tasks.LocalBroadcastManager
import org.tasks.date.DateTimeUtils.newDate
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.newDateUtc
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) {
suspend fun handleRepeat(task: Task) {
val recurrence = task.sanitizedRecurrence()
val repeatAfterCompletion = task.repeatAfterCompletion()
if (!recurrence.isNullOrBlank()) {
val newDueDate: Long
val rrule: RRule
try {
rrule = initRRule(task.getRecurrenceWithoutFrom())
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)) {
return
}
val count = rrule.count
if (count == 1) {
return
}
if (count > 1) {
rrule.count = count - 1
task.setRecurrence(rrule, repeatAfterCompletion)
}
task.reminderSnooze = 0L
task.completionDate = 0L
task.setDueDateAdjustingHideUntil(newDueDate)
gcalHelper.rescheduleRepeatingTask(task)
taskDao.save(task)
alarmService.rescheduleAlarms(task.id, oldDueDate, newDueDate)
localBroadcastManager.broadcastRepeat(task.id, oldDueDate, newDueDate)
}
}
companion object {
private val weekdayCompare = Comparator { object1: WeekdayNum, object2: WeekdayNum -> object1.wday.javaDayNum - object2.wday.javaDayNum }
private fun repeatFinished(newDueDate: Long, repeatUntil: Long): Boolean {
return (repeatUntil > 0
&& newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay()))
}
/** 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.freq)
val startDateAsDV = setUpStartDateAsDV(task, original)
return if (rrule.freq == Frequency.HOURLY || rrule.freq == Frequency.MINUTELY) {
handleSubdayRepeat(original, rrule)
} else if (rrule.freq == Frequency.WEEKLY && rrule.byDay.size > 0 && repeatAfterCompletion) {
handleWeeklyRepeatAfterComplete(rrule, original, task.hasDueTime())
} else if (rrule.freq == Frequency.MONTHLY && rrule.byDay.isEmpty()) {
handleMonthlyRepeat(original, startDateAsDV, task.hasDueTime(), rrule)
} else {
invokeRecurrence(rrule, original, startDateAsDV)
}
}
private fun handleWeeklyRepeatAfterComplete(
rrule: RRule, original: DateTime, hasDueTime: Boolean): Long {
val byDay = rrule.byDay
var newDate = original.millis
newDate += DateUtilities.ONE_WEEK * (rrule.interval - 1)
var date = DateTime(newDate)
Collections.sort(byDay, weekdayCompare)
val next = findNextWeekday(byDay, date)
do {
date = date.plusDays(1)
} while (date.dayOfWeek != next.wday.javaDayNum)
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: DateValue, hasDueTime: Boolean, rrule: RRule): Long {
return if (original.isLastDayOfMonth) {
val interval = rrule.interval
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(rrule, original, startDateAsDV)
}
}
private fun findNextWeekday(byDay: List<WeekdayNum>, date: DateTime): WeekdayNum {
val next = byDay[0]
for (weekday in byDay) {
if (weekday.wday.javaDayNum > date.dayOfWeek) {
return weekday
}
}
return next
}
private fun invokeRecurrence(rrule: RRule, original: DateTime, startDateAsDV: DateValue): Long {
var newDueDate: Long = -1
val iterator = RecurrenceIteratorFactory.createRecurrenceIterator(
rrule, startDateAsDV, TimeZone.getDefault())
var nextDate: DateValue
for (i in 0..9) { // ten tries then we give up
if (!iterator.hasNext()) {
return -1
}
nextDate = iterator.next()
if (nextDate.compareTo(startDateAsDV) == 0) {
continue
}
newDueDate = buildNewDueDate(original, nextDate)
// detect if we finished
if (newDueDate > original.millis) {
break
}
}
return newDueDate
}
/** Compute long due date from DateValue */
private fun buildNewDueDate(original: DateTime, nextDate: DateValue): Long {
val newDueDate: Long
if (nextDate is DateTimeValueImpl) {
var date = newDateUtc(
nextDate.year(),
nextDate.month(),
nextDate.day(),
nextDate.hour(),
nextDate.minute(),
nextDate.second())
.toLocal()
// 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,
newDate(nextDate.year(), nextDate.month(), nextDate.day()).millis)
}
return newDueDate
}
/** Initialize RRule instance */
@Throws(ParseException::class)
private fun initRRule(recurrence: String?): RRule {
val rrule = RRule(recurrence)
// handle the iCalendar "byDay" field differently depending on if
// we are weekly or otherwise
if (rrule.freq != Frequency.WEEKLY && rrule.freq != Frequency.MONTHLY) {
rrule.byDay = emptyList()
}
return rrule
}
/** Set up repeat start date */
private fun setUpStartDate(
task: Task, repeatAfterCompletion: Boolean, frequency: Frequency): DateTime {
return if (repeatAfterCompletion) {
var startDate = if (task.isCompleted) newDateTime(task.completionDate) else newDateTime()
if (task.hasDueTime() && frequency != Frequency.HOURLY && frequency != 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): DateValue {
return if (task.hasDueTime()) {
DateTimeValueImpl(
startDate.year,
startDate.monthOfYear,
startDate.dayOfMonth,
startDate.hourOfDay,
startDate.minuteOfHour,
startDate.secondOfMinute)
} else {
DateValueImpl(
startDate.year, startDate.monthOfYear, startDate.dayOfMonth)
}
}
private fun handleSubdayRepeat(startDate: DateTime, rrule: RRule): Long {
val millis: Long = when (rrule.freq) {
Frequency.HOURLY -> DateUtilities.ONE_HOUR
Frequency.MINUTELY -> DateUtilities.ONE_MINUTE
else -> throw RuntimeException(
"Error handing subday repeat: " + rrule.freq) // $NON-NLS-1$
}
val newDueDate = startDate.millis + millis * rrule.interval
return createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, newDueDate)
}
}
}

@ -45,7 +45,7 @@ class AdvancedRepeatTest {
Task.URGENCY_SPECIFIC_DAY_TIME, DateTime(2010, 8, 1, 10, 4, 0).millis)
task!!.dueDate = dayWithTime
val nextDayWithTime = dayWithTime + DateUtilities.ONE_DAY
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule!!.toIcal(), false)
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, rrule!!.toIcal(), false)
assertDateTimeEquals(nextDayWithTime, nextDueDate)
}
@ -63,7 +63,7 @@ class AdvancedRepeatTest {
var nextDayWithTimeLong = todayWithTime.millis
nextDayWithTimeLong += DateUtilities.ONE_DAY
nextDayWithTimeLong = nextDayWithTimeLong / 1000L * 1000
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule!!.toIcal(), true)
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, rrule!!.toIcal(), true)
assertDateTimeEquals(nextDayWithTimeLong, nextDueDate)
}
@ -194,7 +194,7 @@ class AdvancedRepeatTest {
@Throws(ParseException::class)
private fun computeNextDueDate(fromComplete: Boolean) {
nextDueDate = RepeatTaskHelper.computeNextDueDate(task, rrule!!.toIcal(), fromComplete)
nextDueDate = RepeatTaskHelper.computeNextDueDate(task!!, rrule!!.toIcal(), fromComplete)
}
private fun buildRRule(interval: Int, freq: Frequency, vararg weekdays: Weekday) {

@ -4,9 +4,11 @@ import android.annotation.SuppressLint
import com.google.ical.values.RRule
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDaoBlocking
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@ -23,9 +25,10 @@ import org.tasks.makers.TaskMaker.newTask
import org.tasks.time.DateTime
import java.text.ParseException
@ExperimentalCoroutinesApi
@SuppressLint("NewApi")
class RepeatTaskHelperTest {
private lateinit var taskDao: TaskDaoBlocking
private lateinit var taskDao: TaskDao
private lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var alarmService: AlarmService
private lateinit var gCalHelper: GCalHelper
@ -34,7 +37,7 @@ class RepeatTaskHelperTest {
@Before
fun setUp() {
taskDao = Mockito.mock(TaskDaoBlocking::class.java)
taskDao = Mockito.mock(TaskDao::class.java)
alarmService = Mockito.mock(AlarmService::class.java)
gCalHelper = Mockito.mock(GCalHelper::class.java)
localBroadcastManager = Mockito.mock(LocalBroadcastManager::class.java)
@ -48,7 +51,7 @@ class RepeatTaskHelperTest {
}
@Test
fun noRepeat() {
fun noRepeat() = runBlockingTest {
helper.handleRepeat(newTask(with(DUE_TIME, DateTime(2017, 10, 4, 13, 30))))
}
@ -90,7 +93,7 @@ class RepeatTaskHelperTest {
@Test
@Throws(ParseException::class)
fun testMinutelyLastOccurrence() {
fun testMinutelyLastOccurrence() = runBlockingTest {
val task = newTask(
with(ID, 1L),
with(DUE_TIME, DateTime(2017, 10, 4, 13, 30)),
@ -177,7 +180,7 @@ class RepeatTaskHelperTest {
task, DateTime(2017, 1, 31, 13, 30, 1), DateTime(2017, 2, 28, 13, 30, 1))
}
private fun repeatAndVerify(task: Task, oldDueDate: DateTime, newDueDate: DateTime) {
private fun repeatAndVerify(task: Task, oldDueDate: DateTime, newDueDate: DateTime) = runBlockingTest {
helper.handleRepeat(task)
mocks.verify(gCalHelper).rescheduleRepeatingTask(task)
mocks.verify(alarmService).rescheduleAlarms(1, oldDueDate.millis, newDueDate.millis)

Loading…
Cancel
Save