fix for AST-167: allowing 'by day' type functionality for all repeats

pull/14/head
Tim Su 14 years ago
parent 9b6cbaf1b5
commit 1c0f60d9b6

@ -4,6 +4,7 @@ import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import android.app.Activity;
import android.view.LayoutInflater;
@ -65,6 +66,8 @@ public class RepeatControlSet implements TaskEditControlSet {
@Autowired
ExceptionService exceptionService;
boolean setInterval = false;
// --- implementation
public RepeatControlSet(final Activity activity, ViewGroup parent) {
@ -111,10 +114,22 @@ public class RepeatControlSet implements TaskEditControlSet {
repeatValueClick();
}
});
interval.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parentView, View view, int position, long id) {
daysOfWeekContainer.setVisibility(position == INTERVAL_WEEKS ? View.VISIBLE : View.GONE);
if(setInterval) {
setInterval = false;
return;
}
if(position == INTERVAL_WEEKS) {
int dayOfWeek = new Date().getDay();
for(int i = 0; i < 7; i++)
daysOfWeek[i].setChecked(i == dayOfWeek);
} else {
for(int i = 0; i < 7; i++)
daysOfWeek[i].setChecked(true);
}
}
@Override
@ -122,6 +137,7 @@ public class RepeatControlSet implements TaskEditControlSet {
//
}
});
daysOfWeekContainer.setVisibility(View.VISIBLE);
}
/** Set up the repeat value button */
@ -172,17 +188,6 @@ public class RepeatControlSet implements TaskEditControlSet {
break;
case WEEKLY: {
interval.setSelection(INTERVAL_WEEKS);
// clear all day of week checks, then update them
for(int i = 0; i < 7; i++)
daysOfWeek[i].setChecked(false);
for(WeekdayNum day : rrule.getByDay()) {
for(int i = 0; i < 7; i++)
if(daysOfWeek[i].getTag() == day.wday)
daysOfWeek[i].setChecked(true);
}
break;
}
case MONTHLY:
@ -196,6 +201,19 @@ public class RepeatControlSet implements TaskEditControlSet {
exceptionService.reportError("repeat-unhandled-rule", //$NON-NLS-1$
new Exception("Unhandled rrule frequency: " + recurrence)); //$NON-NLS-1$
}
// clear all day of week checks, then update them
for(int i = 0; i < 7; i++)
daysOfWeek[i].setChecked(false);
for(WeekdayNum day : rrule.getByDay()) {
for(int i = 0; i < 7; i++)
if(daysOfWeek[i].getTag() == day.wday)
daysOfWeek[i].setChecked(true);
}
// suppress first call to interval.onItemSelected
setInterval = true;
} catch (ParseException e) {
recurrence = ""; //$NON-NLS-1$
exceptionService.reportError("repeat-parse-exception", e); //$NON-NLS-1$
@ -226,11 +244,6 @@ public class RepeatControlSet implements TaskEditControlSet {
break;
case INTERVAL_WEEKS: {
rrule.setFreq(Frequency.WEEKLY);
ArrayList<WeekdayNum> days = new ArrayList<WeekdayNum>();
for(int i = 0; i < daysOfWeek.length; i++)
if(daysOfWeek[i].isChecked())
days.add(new WeekdayNum(0, (Weekday)daysOfWeek[i].getTag()));
rrule.setByDay(days);
break;
}
case INTERVAL_MONTHS:
@ -239,6 +252,13 @@ public class RepeatControlSet implements TaskEditControlSet {
case INTERVAL_HOURS:
rrule.setFreq(Frequency.HOURLY);
}
ArrayList<WeekdayNum> days = new ArrayList<WeekdayNum>();
for(int i = 0; i < daysOfWeek.length; i++)
if(daysOfWeek[i].isChecked())
days.add(new WeekdayNum(0, (Weekday)daysOfWeek[i].getTag()));
rrule.setByDay(days);
result = rrule.toIcal();
}
task.setValue(Task.RECURRENCE, result);

@ -92,8 +92,8 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo
}
interval = "<b>" + interval + "</b>"; //$NON-NLS-1$//$NON-NLS-2$
if(rrule.getFreq() == Frequency.WEEKLY) {
List<WeekdayNum> byDay = rrule.getByDay();
List<WeekdayNum> byDay = rrule.getByDay();
if(rrule.getFreq() == Frequency.WEEKLY || byDay.size() != 7) {
if(byDay.size() > 0) {
StringBuilder byDayString = new StringBuilder();
DateFormatSymbols dfs = new DateFormatSymbols();

@ -1,7 +1,9 @@
package com.todoroo.astrid.repeats;
import java.text.ParseException;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import android.content.BroadcastReceiver;
@ -15,6 +17,7 @@ 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.service.Autowired;
import com.todoroo.andlib.service.DependencyInjectionService;
import com.todoroo.andlib.service.ExceptionService;
@ -46,83 +49,116 @@ public class RepeatTaskCompleteListener extends BroadcastReceiver {
String recurrence = task.getValue(Task.RECURRENCE);
if(recurrence != null && recurrence.length() > 0) {
DateValue repeatFrom;
Date repeatFromDate = new Date();
DateValue today = new DateValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate());
if(task.hasDueDate() && !task.getFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION)) {
repeatFromDate = new Date(task.getValue(Task.DUE_DATE));
if(task.hasDueTime()) {
repeatFrom = new DateTimeValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate(),
repeatFromDate.getHours(), repeatFromDate.getMinutes(), repeatFromDate.getSeconds());
} else {
repeatFrom = new DateValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate());
}
long newDueDate;
try {
newDueDate = computeNextDueDate(task, recurrence);
if(newDueDate == -1)
return;
} catch (ParseException e) {
exceptionService.reportError("repeat-parse", e); //$NON-NLS-1$
return;
}
long hideUntil = task.getValue(Task.HIDE_UNTIL);
if(hideUntil > 0 && task.getValue(Task.DUE_DATE) > 0) {
hideUntil += newDueDate - task.getValue(Task.DUE_DATE);
}
// clear recurrence from completed task so it can be re-commpleted
task.setValue(Task.RECURRENCE, ""); //$NON-NLS-1$
taskService.save(task, false);
// clone to create new task
task = taskService.clone(task);
task.setValue(Task.DUE_DATE, newDueDate);
task.setValue(Task.HIDE_UNTIL, hideUntil);
task.setValue(Task.COMPLETION_DATE, 0L);
task.setValue(Task.TIMER_START, 0L);
task.setValue(Task.ELAPSED_SECONDS, 0);
taskService.save(task, false);
}
}
public static long computeNextDueDate(Task task, String recurrence) throws ParseException {
DateValue repeatFrom;
Date repeatFromDate = new Date();
DateValue today = new DateValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate());
if(task.hasDueDate() && !task.getFlag(Task.FLAGS, Task.FLAG_REPEAT_AFTER_COMPLETION)) {
repeatFromDate = new Date(task.getValue(Task.DUE_DATE));
if(task.hasDueTime()) {
repeatFrom = new DateTimeValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate(),
repeatFromDate.getHours(), repeatFromDate.getMinutes(), repeatFromDate.getSeconds());
} else {
repeatFrom = today;
repeatFrom = new DateValueImpl(repeatFromDate.getYear() + 1900,
repeatFromDate.getMonth() + 1, repeatFromDate.getDate());
}
} else {
repeatFrom = today;
}
// invoke the recurrence iterator
try {
long newDueDate;
RRule rrule = new RRule(recurrence);
if(rrule.getFreq() == Frequency.HOURLY) {
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
repeatFromDate.getTime() + DateUtilities.ONE_HOUR * rrule.getInterval());
} else {
RecurrenceIterator iterator = RecurrenceIteratorFactory.createRecurrenceIterator(rrule,
repeatFrom, TimeZone.getDefault());
DateValue nextDate;
if(repeatFrom.compareTo(today) < 0) {
iterator.advanceTo(today);
if(!iterator.hasNext())
return;
nextDate = iterator.next();
} else {
iterator.advanceTo(repeatFrom);
if(!iterator.hasNext())
return;
nextDate = iterator.next();
nextDate = iterator.next();
}
if(nextDate instanceof DateTimeValueImpl) {
DateTimeValueImpl newDateTime = (DateTimeValueImpl)nextDate;
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
Date.UTC(newDateTime.year() - 1900, newDateTime.month() - 1,
newDateTime.day(), newDateTime.hour(),
newDateTime.minute(), newDateTime.second()));
} else {
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY,
new Date(nextDate.year() - 1900, nextDate.month() - 1,
nextDate.day()).getTime());
}
}
long hideUntil = task.getValue(Task.HIDE_UNTIL);
if(hideUntil > 0 && task.getValue(Task.DUE_DATE) > 0) {
hideUntil += newDueDate - task.getValue(Task.DUE_DATE);
}
// clear recurrence from completed task so it can be re-commpleted
task.setValue(Task.RECURRENCE, ""); //$NON-NLS-1$
taskService.save(task, false);
// clone to create new task
task = taskService.clone(task);
task.setValue(Task.DUE_DATE, newDueDate);
task.setValue(Task.HIDE_UNTIL, hideUntil);
task.setValue(Task.COMPLETION_DATE, 0L);
task.setValue(Task.TIMER_START, 0L);
task.setValue(Task.ELAPSED_SECONDS, 0);
taskService.save(task, false);
} catch (ParseException e) {
exceptionService.reportError("recurrence-rule: " + recurrence, e); //$NON-NLS-1$
// invoke the recurrence iterator
long newDueDate;
RRule rrule = new RRule(recurrence);
// handle the iCalendar "byDay" field differently depending on if
// we are weekly or otherwise
List<WeekdayNum> byDay = null;
if(rrule.getFreq() != Frequency.WEEKLY) {
byDay = rrule.getByDay();
rrule.setByDay(Collections.EMPTY_LIST);
}
if(rrule.getFreq() == Frequency.HOURLY) {
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
repeatFromDate.getTime() + DateUtilities.ONE_HOUR * rrule.getInterval());
} else {
RecurrenceIterator iterator = RecurrenceIteratorFactory.createRecurrenceIterator(rrule,
repeatFrom, TimeZone.getDefault());
DateValue nextDate = repeatFrom;
if(repeatFrom.compareTo(today) < 0)
iterator.advanceTo(today);
for(int i = 0; i < 10; i++) { // ten tries then we give up
if(!iterator.hasNext())
return -1;
nextDate = iterator.next();
if(nextDate.compareTo(repeatFrom) != 0)
break;
}
System.err.println("REPEAT started " + repeatFrom + ", ended " + nextDate); //$NON-NLS-1$ //$NON-NLS-2$
if(nextDate instanceof DateTimeValueImpl) {
DateTimeValueImpl newDateTime = (DateTimeValueImpl)nextDate;
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME,
Date.UTC(newDateTime.year() - 1900, newDateTime.month() - 1,
newDateTime.day(), newDateTime.hour(),
newDateTime.minute(), newDateTime.second()));
} else {
newDueDate = task.createDueDate(Task.URGENCY_SPECIFIC_DAY,
new Date(nextDate.year() - 1900, nextDate.month() - 1,
nextDate.day()).getTime());
}
}
// what we do with the by day information is to add days until
// weekday equals one of this list
if(byDay != null && byDay.size() > 0) {
Date newDueDateDate = new Date(newDueDate);
outer: for(int i = 0; i < 7; i++) {
int weekday = newDueDateDate.getDay();
for(WeekdayNum wdn : byDay)
if(wdn.wday.jsDayNum == weekday)
break outer;
newDueDateDate.setDate(newDueDateDate.getDate() + 1);
}
newDueDate = newDueDateDate.getTime();
}
return newDueDate;
}
}

@ -15,12 +15,12 @@ import android.graphics.Paint;
import android.text.Html;
import android.text.util.Linkify;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.CheckBox;
@ -308,7 +308,10 @@ public class TaskAdapter extends CursorAdapter implements Filterable {
// importance bar
final View importanceView = viewHolder.importance; {
int value = task.getValue(Task.IMPORTANCE);
importanceView.setBackgroundColor(IMPORTANCE_COLORS[value]);
if(value < IMPORTANCE_COLORS.length)
importanceView.setBackgroundColor(IMPORTANCE_COLORS[value]);
else
importanceView.setBackgroundColor(0);
}
// details and decorations, expanded

@ -153,7 +153,7 @@ public class TaskDao extends GenericDao<Task> {
if(saveSuccessful) {
task.markSaved();
afterSave(task, values, skipHooks);
afterSave(task, values);
}
return saveSuccessful;
@ -208,9 +208,9 @@ public class TaskDao extends GenericDao<Task> {
* @param values values to be persisted to the database
* @param skipHooks whether this save occurs as part of a sync
*/
private void afterSave(Task task, ContentValues values, boolean skipHooks) {
private void afterSave(Task task, ContentValues values) {
if(values.containsKey(Task.COMPLETION_DATE.name) && task.isCompleted())
afterComplete(task, values, skipHooks);
afterComplete(task, values);
else {
ReminderService.getInstance().scheduleAlarm(task);
}
@ -218,9 +218,6 @@ public class TaskDao extends GenericDao<Task> {
Astrid2TaskProvider.notifyDatabaseModification();
ContextManager.getContext().startService(new Intent(ContextManager.getContext(),
TasksWidget.UpdateService.class));
if(skipHooks)
return;
}
/**
@ -230,14 +227,12 @@ public class TaskDao extends GenericDao<Task> {
* @param values
* @param duringSync
*/
private void afterComplete(Task task, ContentValues values, boolean duringSync) {
private void afterComplete(Task task, ContentValues values) {
// send broadcast
if(!duringSync) {
Context context = ContextManager.getContext();
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
context.sendOrderedBroadcast(broadcastIntent, null);
}
Context context = ContextManager.getContext();
Intent broadcastIntent = new Intent(AstridApiConstants.BROADCAST_EVENT_TASK_COMPLETED);
broadcastIntent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, task.getId());
context.sendOrderedBroadcast(broadcastIntent, null);
}
}

@ -254,7 +254,7 @@ public class NumberPicker extends LinearLayout implements OnClickListener,
}
private void changeCurrent(int current, Animation in, Animation out) {
current = notifyChange();
current = notifyChange(current);
// Wrap around the values if we go past the start or end
if (current > mEnd) {
@ -267,9 +267,9 @@ public class NumberPicker extends LinearLayout implements OnClickListener,
updateView();
}
private int notifyChange() {
private int notifyChange(int current) {
if (mListener != null) {
return mListener.onChanged(this, mPrevious, mCurrent);
return mListener.onChanged(this, mCurrent, current);
} else
return mCurrent;
@ -294,7 +294,7 @@ public class NumberPicker extends LinearLayout implements OnClickListener,
if ((val >= mStart) && (val <= mEnd)) {
mPrevious = mCurrent;
mCurrent = val;
notifyChange();
notifyChange(mCurrent);
}
updateView();
}

@ -0,0 +1,113 @@
package com.todoroo.astrid.repeats;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import com.google.ical.values.Frequency;
import com.google.ical.values.RRule;
import com.google.ical.values.Weekday;
import com.google.ical.values.WeekdayNum;
import com.todoroo.andlib.test.TodorooTestCase;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.model.Task;
public class AdvancedRepeatTests extends TodorooTestCase {
public static void assertDatesEqual(long date, long other) {
assertEquals("Expected: " + new Date(date) + ", Actual: " + new Date(other),
date, other);
}
public void testDailyWithDaysOfWeek() throws ParseException {
RRule rrule = new RRule();
rrule.setInterval(1);
rrule.setFreq(Frequency.DAILY);
rrule.setByDay(Collections.singletonList(new WeekdayNum(0, Weekday.FR)));
Task task = new Task();
long thursday = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(113, 7, 1).getTime());
task.setValue(Task.DUE_DATE, thursday);
// repeat once => due date should become friday
long friday = thursday + DateUtilities.ONE_DAY;
long nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(friday, nextDueDate);
// repeat again => due date should be one week from friday
long nextFriday = friday + DateUtilities.ONE_WEEK;
task.setValue(Task.DUE_DATE, friday);
nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(nextFriday, nextDueDate);
// now try with thursday, and repeat every 2 days. expect next friday
rrule.setInterval(2);
task.setValue(Task.DUE_DATE, thursday);
nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(nextFriday, nextDueDate);
// again with friday, expect next friday
task.setValue(Task.DUE_DATE, friday);
nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(nextFriday, nextDueDate);
}
public void testMonthlyWithDaysOfWeek() throws ParseException {
RRule rrule = new RRule();
rrule.setInterval(1);
rrule.setFreq(Frequency.MONTHLY);
rrule.setByDay(Arrays.asList(new WeekdayNum[] {
new WeekdayNum(0, Weekday.SU),
new WeekdayNum(0, Weekday.MO),
new WeekdayNum(0, Weekday.TU),
new WeekdayNum(0, Weekday.WE),
new WeekdayNum(0, Weekday.TH),
new WeekdayNum(0, Weekday.FR),
new WeekdayNum(0, Weekday.SA),
}));
Task task = new Task();
long thursday = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(113, 7, 1).getTime());
task.setValue(Task.DUE_DATE, thursday);
// repeat once => due date should become next month on the first
long nextMonth = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(113, 8, 1).getTime());
long nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(nextMonth, nextDueDate);
// only allow thursdays
rrule.setByDay(Arrays.asList(new WeekdayNum[] {
new WeekdayNum(0, Weekday.TH),
}));
long nextMonthOnThursday = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(113, 8, 5).getTime());
nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(nextMonthOnThursday, nextDueDate);
}
public void testDueDateInPast() throws ParseException {
RRule rrule = new RRule();
rrule.setInterval(1);
rrule.setFreq(Frequency.DAILY);
Task task = new Task();
// repeat once => due date should become tomorrow
long past = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, new Date(110, 7, 1).getTime());
task.setValue(Task.DUE_DATE, past);
long today = task.createDueDate(Task.URGENCY_SPECIFIC_DAY, DateUtilities.now());
long nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(today, nextDueDate);
// test specific day & time
long pastWithTime = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, new Date(110, 7, 1, 10, 4).getTime());
task.setValue(Task.DUE_DATE, pastWithTime);
Date date = new Date(DateUtilities.now() / 1000L * 1000L);
date.setHours(10);
date.setMinutes(4);
long todayWithTime = task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, date.getTime());
nextDueDate = RepeatTaskCompleteListener.computeNextDueDate(task, rrule.toIcal());
assertDatesEqual(todayWithTime + DateUtilities.ONE_DAY, nextDueDate);
}
}
Loading…
Cancel
Save