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/RepeatTaskCompleteListener....

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);
}
}