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.
295 lines
11 KiB
Java
295 lines
11 KiB
Java
/**
|
|
* Copyright (c) 2012 Todoroo Inc
|
|
*
|
|
* See the file "LICENSE" for the full license governing this code.
|
|
*/
|
|
package com.todoroo.astrid.repeats;
|
|
|
|
import android.content.Context;
|
|
import android.content.Intent;
|
|
|
|
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.AlarmTaskRepeatListener;
|
|
import com.todoroo.astrid.api.AstridApiConstants;
|
|
import com.todoroo.astrid.dao.TaskDao;
|
|
import com.todoroo.astrid.data.Task;
|
|
import com.todoroo.astrid.gcal.GCalHelper;
|
|
|
|
import org.tasks.LocalBroadcastManager;
|
|
import org.tasks.injection.BroadcastComponent;
|
|
import org.tasks.injection.InjectingBroadcastReceiver;
|
|
import org.tasks.time.DateTime;
|
|
|
|
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 timber.log.Timber;
|
|
|
|
import static org.tasks.date.DateTimeUtils.newDate;
|
|
import static org.tasks.date.DateTimeUtils.newDateTime;
|
|
import static org.tasks.date.DateTimeUtils.newDateUtc;
|
|
|
|
public class RepeatTaskCompleteListener extends InjectingBroadcastReceiver {
|
|
|
|
public static void broadcast(Context context, long taskId) {
|
|
Intent intent = new Intent(context, RepeatTaskCompleteListener.class);
|
|
intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, taskId);
|
|
context.sendBroadcast(intent);
|
|
}
|
|
|
|
@Inject GCalHelper gcalHelper;
|
|
@Inject TaskDao taskDao;
|
|
@Inject LocalBroadcastManager localBroadcastManager;
|
|
|
|
@Override
|
|
public void onReceive(Context context, Intent intent) {
|
|
super.onReceive(context, intent);
|
|
|
|
long taskId = intent.getLongExtra(AstridApiConstants.EXTRAS_TASK_ID, -1);
|
|
if(taskId == -1) {
|
|
return;
|
|
}
|
|
|
|
Task task = taskDao.fetch(taskId, Task.PROPERTIES);
|
|
if(task == null || !task.isCompleted()) {
|
|
return;
|
|
}
|
|
|
|
String recurrence = task.sanitizedRecurrence();
|
|
boolean repeatAfterCompletion = task.repeatAfterCompletion();
|
|
|
|
if(recurrence != null && recurrence.length() > 0) {
|
|
long newDueDate;
|
|
try {
|
|
newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion);
|
|
if(newDueDate == -1) {
|
|
return;
|
|
}
|
|
} catch (ParseException e) {
|
|
Timber.e(e, e.getMessage());
|
|
return;
|
|
}
|
|
|
|
long oldDueDate = task.getDueDate();
|
|
long repeatUntil = task.getRepeatUntil();
|
|
|
|
if (repeatFinished(newDueDate, repeatUntil)) {
|
|
return;
|
|
}
|
|
|
|
rescheduleTask(gcalHelper, taskDao, task, newDueDate);
|
|
|
|
AlarmTaskRepeatListener.broadcast(context, taskId, oldDueDate, newDueDate);
|
|
|
|
localBroadcastManager.broadcastRepeat(task.getId(), oldDueDate, newDueDate);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void inject(BroadcastComponent component) {
|
|
component.inject(this);
|
|
}
|
|
|
|
private static boolean repeatFinished(long newDueDate, long repeatUntil) {
|
|
return repeatUntil > 0 && newDateTime(newDueDate).startOfDay().isAfter(newDateTime(repeatUntil).startOfDay());
|
|
}
|
|
|
|
private static void rescheduleTask(GCalHelper gcalHelper, TaskDao taskDao, Task task, long newDueDate) {
|
|
task.setReminderSnooze(0L);
|
|
task.setCompletionDate(0L);
|
|
task.setDueDateAdjustingHideUntil(newDueDate);
|
|
|
|
gcalHelper.rescheduleRepeatingTask(task);
|
|
taskDao.save(task);
|
|
}
|
|
|
|
/** Compute next due date */
|
|
public 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) {
|
|
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 final Comparator<WeekdayNum> weekdayCompare = (object1, object2) -> object1.wday.javaDayNum - object2.wday.javaDayNum;
|
|
|
|
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.setByDay(Collections.EMPTY_LIST);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
}
|