From 1c0f60d9b6e8a63ca6a31c36f6ab9c5bc2f053df Mon Sep 17 00:00:00 2001 From: Tim Su Date: Fri, 6 Aug 2010 03:47:15 -0700 Subject: [PATCH] fix for AST-167: allowing 'by day' type functionality for all repeats --- .../astrid/repeats/RepeatControlSet.java | 54 ++++-- .../astrid/repeats/RepeatDetailExposer.java | 4 +- .../repeats/RepeatTaskCompleteListener.java | 180 +++++++++++------- .../todoroo/astrid/adapter/TaskAdapter.java | 9 +- .../src/com/todoroo/astrid/dao/TaskDao.java | 21 +- .../com/todoroo/astrid/ui/NumberPicker.java | 8 +- .../astrid/repeats/AdvancedRepeatTests.java | 113 +++++++++++ 7 files changed, 278 insertions(+), 111 deletions(-) create mode 100644 tests/src/com/todoroo/astrid/repeats/AdvancedRepeatTests.java diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java index 8b81b7f17..2b1e69d7b 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatControlSet.java @@ -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 days = new ArrayList(); - 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 days = new ArrayList(); + 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); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java index 46d3af6f7..4d332b583 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatDetailExposer.java @@ -92,8 +92,8 @@ public class RepeatDetailExposer extends BroadcastReceiver implements DetailExpo } interval = "" + interval + ""; //$NON-NLS-1$//$NON-NLS-2$ - if(rrule.getFreq() == Frequency.WEEKLY) { - List byDay = rrule.getByDay(); + List byDay = rrule.getByDay(); + if(rrule.getFreq() == Frequency.WEEKLY || byDay.size() != 7) { if(byDay.size() > 0) { StringBuilder byDayString = new StringBuilder(); DateFormatSymbols dfs = new DateFormatSymbols(); diff --git a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java index 64e37d979..ea249635a 100644 --- a/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java +++ b/astrid/plugin-src/com/todoroo/astrid/repeats/RepeatTaskCompleteListener.java @@ -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 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; } } diff --git a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java index 2542eac1c..0c26153f1 100644 --- a/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java +++ b/astrid/src/com/todoroo/astrid/adapter/TaskAdapter.java @@ -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 diff --git a/astrid/src/com/todoroo/astrid/dao/TaskDao.java b/astrid/src/com/todoroo/astrid/dao/TaskDao.java index 07f626727..cd94a9cd6 100644 --- a/astrid/src/com/todoroo/astrid/dao/TaskDao.java +++ b/astrid/src/com/todoroo/astrid/dao/TaskDao.java @@ -153,7 +153,7 @@ public class TaskDao extends GenericDao { if(saveSuccessful) { task.markSaved(); - afterSave(task, values, skipHooks); + afterSave(task, values); } return saveSuccessful; @@ -208,9 +208,9 @@ public class TaskDao extends GenericDao { * @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 { Astrid2TaskProvider.notifyDatabaseModification(); ContextManager.getContext().startService(new Intent(ContextManager.getContext(), TasksWidget.UpdateService.class)); - - if(skipHooks) - return; } /** @@ -230,14 +227,12 @@ public class TaskDao extends GenericDao { * @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); } } diff --git a/astrid/src/com/todoroo/astrid/ui/NumberPicker.java b/astrid/src/com/todoroo/astrid/ui/NumberPicker.java index f0db4d326..932cf9e4e 100644 --- a/astrid/src/com/todoroo/astrid/ui/NumberPicker.java +++ b/astrid/src/com/todoroo/astrid/ui/NumberPicker.java @@ -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(); } diff --git a/tests/src/com/todoroo/astrid/repeats/AdvancedRepeatTests.java b/tests/src/com/todoroo/astrid/repeats/AdvancedRepeatTests.java new file mode 100644 index 000000000..fd78a034a --- /dev/null +++ b/tests/src/com/todoroo/astrid/repeats/AdvancedRepeatTests.java @@ -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); + } +}