Reminder test improvements

pull/127/merge
Alex Baker 9 years ago
parent c02164ad7a
commit f46aebb784

@ -1,71 +0,0 @@
package com.todoroo.astrid.reminders;
import android.support.test.runner.AndroidJUnit4;
import com.todoroo.astrid.data.Task;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import static android.support.test.InstrumentationRegistry.getTargetContext;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE;
import static com.todoroo.astrid.reminders.ReminderService.NO_ALARM;
import static junit.framework.Assert.assertEquals;
import static org.tasks.makers.TaskMaker.DUE_DATE;
import static org.tasks.makers.TaskMaker.DUE_TIME;
import static org.tasks.makers.TaskMaker.REMINDERS;
import static org.tasks.makers.TaskMaker.REMINDER_LAST;
import static org.tasks.makers.TaskMaker.newTask;
@RunWith(AndroidJUnit4.class)
public class NotifyAtDeadlineTest {
private ReminderService reminderService;
@Before
public void setUp() {
Preferences preferences = new Preferences(getTargetContext(), null);
reminderService = new ReminderService(preferences, null);
}
@Test
public void testNoReminderWhenNoDueDate() {
Task task = newTask(with(REMINDERS, NOTIFY_AT_DEADLINE));
assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task));
}
@Test
public void testNoReminderWhenNotifyAtDeadlineFlagNotSet() {
Task task = newTask(with(DUE_TIME, new DateTime(2014, 1, 24, 19, 23)));
assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task));
}
@Test
public void testScheduleReminderAtDueTime() {
final DateTime dueDate = new DateTime(2014, 1, 24, 19, 23);
Task task = newTask(with(DUE_TIME, dueDate), with(REMINDERS, NOTIFY_AT_DEADLINE));
assertEquals(dueDate.plusSeconds(1).getMillis(), reminderService.calculateNextDueDateReminder(task));
}
@Test
public void testScheduleReminderAtDefaultDueTime() {
final DateTime dueDate = new DateTime(2015, 12, 29, 12, 0);
Task task = newTask(with(DUE_DATE, dueDate), with(REMINDERS, NOTIFY_AT_DEADLINE));
assertEquals(dueDate.withHourOfDay(18).getMillis(),
reminderService.calculateNextDueDateReminder(task));
}
@Test
public void testNoReminderIfAlreadyRemindedPastDueDate() {
final DateTime dueDate = new DateTime(2015, 12, 29, 19, 23);
Task task = newTask(
with(DUE_TIME, dueDate),
with(REMINDER_LAST, dueDate.plusSeconds(1)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
assertEquals(NO_ALARM, reminderService.calculateNextDueDateReminder(task));
}
}

@ -1,139 +0,0 @@
package com.todoroo.astrid.reminders;
import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.tasks.Freeze;
import org.tasks.Snippet;
import org.tasks.jobs.JobManager;
import org.tasks.jobs.Reminder;
import org.tasks.makers.TaskMaker;
import org.tasks.preferences.Preferences;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_DUE;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static junit.framework.Assert.assertEquals;
import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.tasks.makers.TaskMaker.newTask;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@RunWith(AndroidJUnit4.class)
public class ReminderAlarmSchedulerTest {
private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
private JobManager jobManager;
private ReminderAlarmScheduler scheduler;
@Before
public void before() {
jobManager = mock(JobManager.class);
Preferences preferences = mock(Preferences.class);
when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg());
scheduler = new ReminderAlarmScheduler(jobManager, preferences);
}
@After
public void after() {
verifyNoMoreInteractions(jobManager);
}
@Test
public void scheduleFirstReminder() {
long now = currentTimeMillis();
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, 0);
verify(jobManager).scheduleReminder(now, true);
}
@Test
public void dontScheduleLaterReminder() {
long now = currentTimeMillis();
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, 0);
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now + ONE_MINUTE, 0);
verify(jobManager).scheduleReminder(now, true);
}
@Test
public void rescheduleNewerReminder() {
long now = currentTimeMillis();
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, 0);
scheduler.createAlarm(newTask(with(TaskMaker.ID, 2L)), now - ONE_MINUTE, 0);
InOrder order = inOrder(jobManager);
order.verify(jobManager).scheduleReminder(now, true);
order.verify(jobManager).scheduleReminder(now - ONE_MINUTE, true);
}
@Test
public void removeLastReminderCancelsJob() {
long now = currentTimeMillis();
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, 0);
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), 0, 0);
InOrder order = inOrder(jobManager);
order.verify(jobManager).scheduleReminder(now, true);
order.verify(jobManager).cancelReminders();
}
@Test
public void removePastRemindersReturnsPastReminder() {
long now = currentTimeMillis();
Freeze.freezeAt(now).thawAfter(new Snippet() {{
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, TYPE_DUE);
List<Reminder> reminders = scheduler.removePastReminders();
verify(jobManager).scheduleReminder(now, true);
assertEquals(singletonList(new Reminder(1, now, TYPE_DUE)), reminders);
}});
}
@Test
public void dontRescheduleForSecondJobAtSameTime() {
long now = currentTimeMillis();
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, TYPE_DUE);
scheduler.createAlarm(newTask(with(TaskMaker.ID, 2L)), now, TYPE_DUE);
verify(jobManager).scheduleReminder(now, true);
}
@Test
public void removePastRemindersReturnsPastRemindersAtSameTime() {
long now = currentTimeMillis();
Freeze.freezeAt(now).thawAfter(new Snippet() {{
scheduler.createAlarm(newTask(with(TaskMaker.ID, 1L)), now, TYPE_DUE);
scheduler.createAlarm(newTask(with(TaskMaker.ID, 2L)), now, TYPE_DUE);
List<Reminder> reminders = scheduler.removePastReminders();
verify(jobManager).scheduleReminder(now, true);
assertEquals(asList(new Reminder(1, now, TYPE_DUE), new Reminder(2, now, TYPE_DUE)), reminders);
}});
}
}

@ -1,41 +1,68 @@
/**
* Copyright (c) 2012 Todoroo Inc
*
* See the file "LICENSE" for the full license governing this code.
*/
package com.todoroo.astrid.reminders; package com.todoroo.astrid.reminders;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import org.junit.After; import org.junit.After;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.tasks.Snippet;
import org.tasks.injection.InjectingTestCase; import org.tasks.injection.InjectingTestCase;
import org.tasks.injection.TestComponent; import org.tasks.injection.TestComponent;
import org.tasks.jobs.JobQueue;
import org.tasks.jobs.Reminder;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import static com.natpryce.makeiteasy.MakeItEasy.with;
import static com.todoroo.astrid.data.Task.NOTIFY_AFTER_DEADLINE;
import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_RANDOM;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.tasks.Freeze.freezeClock; import static org.tasks.Freeze.freezeClock;
import static org.tasks.Freeze.thaw;
import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.date.DateTimeUtils.newDateTime;
import static org.tasks.makers.TaskMaker.COMPLETION_TIME;
import static org.tasks.makers.TaskMaker.DELETION_TIME;
import static org.tasks.makers.TaskMaker.DUE_DATE;
import static org.tasks.makers.TaskMaker.DUE_TIME;
import static org.tasks.makers.TaskMaker.ID;
import static org.tasks.makers.TaskMaker.RANDOM_REMINDER_PERIOD;
import static org.tasks.makers.TaskMaker.REMINDERS;
import static org.tasks.makers.TaskMaker.REMINDER_LAST;
import static org.tasks.makers.TaskMaker.SNOOZE_TIME;
import static org.tasks.makers.TaskMaker.newTask;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class ReminderServiceTest extends InjectingTestCase { public class ReminderServiceTest extends InjectingTestCase {
@Inject TaskDao taskDao; @Inject Preferences preferences;
@Inject ReminderService reminderService;
@Override private ReminderService service;
public void setUp() { private JobQueue<Reminder> jobs;
super.setUp();
freezeClock(); @Before
public void before() {
jobs = mock(JobQueue.class);
service = new ReminderService(preferences, jobs);
}
@After
public void after() {
verifyNoMoreInteractions(jobs);
} }
@Override @Override
@ -43,235 +70,246 @@ public class ReminderServiceTest extends InjectingTestCase {
component.inject(this); component.inject(this);
} }
@After @Test
public void tearDown() { public void dontScheduleDueDateReminderWhenFlagNotSet() {
thaw(); service.scheduleAlarm(null, newTask(with(ID, 1L), with(DUE_TIME, newDateTime())));
verify(jobs).cancel(1);
} }
@Test @Test
public void testNoReminders() { public void dontScheduleDueDateReminderWhenTimeNotSet() {
reminderService.setScheduler(new NoAlarmExpected()); service.scheduleAlarm(null, newTask(with(ID, 1L), with(REMINDERS, NOTIFY_AT_DEADLINE)));
Task task = new Task(); verify(jobs).cancel(1);
task.setTitle("water");
task.setReminderFlags(0);
task.setReminderPeriod(0L);
taskDao.save(task);
reminderService.scheduleAlarm(taskDao, task);
} }
@Test @Test
public void testDueDates() { public void schedulePastDueDate() {
reminderService.setScheduler(new AlarmExpected() { Task task = newTask(
@Override with(ID, 1L),
public void createAlarm(Task task, long time, int type) { with(DUE_TIME, newDateTime().minusDays(1)),
if (time == ReminderService.NO_ALARM) with(REMINDERS, NOTIFY_AT_DEADLINE));
return; service.scheduleAlarm(null, task);
super.createAlarm(task, time, type);
assertEquals((long) task.getDueDate(), time); InOrder order = inOrder(jobs);
assertEquals(type, ReminderService.TYPE_DUE); order.verify(jobs).cancel(1);
} order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
});
// test due date in the past
final Task task = new Task();
task.setTitle("water");
task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY);
task.setReminderFlags(Task.NOTIFY_AT_DEADLINE);
taskDao.save(task);
// test due date in the future
task.setDueDate(DateUtilities.now() + DateUtilities.ONE_DAY);
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
} }
@Test @Test
public void testRandom() { public void scheduleFutureDueDate() {
// test random Task task = newTask(
final Task task = new Task(); with(ID, 1L),
task.setTitle("water"); with(DUE_TIME, newDateTime().plusDays(1)),
task.setReminderPeriod(DateUtilities.ONE_WEEK); with(REMINDERS, NOTIFY_AT_DEADLINE));
reminderService.setScheduler(new AlarmExpected() { service.scheduleAlarm(null, task);
@Override
public void createAlarm(Task task, long time, int type) { InOrder order = inOrder(jobs);
if (time == ReminderService.NO_ALARM) order.verify(jobs).cancel(1);
return; order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now());
assertTrue(time < DateUtilities.now() + 1.2 * DateUtilities.ONE_WEEK);
assertEquals(type, ReminderService.TYPE_RANDOM);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
} }
@Test @Test
public void testOverdue() { public void scheduleReminderAtDefaultDueTime() {
// test due date in the future DateTime now = newDateTime();
reminderService.setScheduler(new AlarmExpected() { Task task = newTask(
@Override with(ID, 1L),
public void createAlarm(Task task, long time, int type) { with(DUE_DATE, now),
if (time == ReminderService.NO_ALARM) with(REMINDERS, NOTIFY_AT_DEADLINE));
return; service.scheduleAlarm(null, task);
super.createAlarm(task, time, type);
assertTrue(time > task.getDueDate()); InOrder order = inOrder(jobs);
assertTrue(time < task.getDueDate() + DateUtilities.ONE_DAY); order.verify(jobs).cancel(1);
assertEquals(type, ReminderService.TYPE_OVERDUE); order.verify(jobs).add(new Reminder(1, now.startOfDay().withHourOfDay(18).getMillis(), ReminderService.TYPE_DUE));
}
});
final Task task = new Task();
task.setTitle("water");
task.setDueDate(DateUtilities.now() + DateUtilities.ONE_DAY);
task.setReminderFlags(Task.NOTIFY_AFTER_DEADLINE);
taskDao.save(task);
// test due date in the past
task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY);
reminderService.setScheduler(new AlarmExpected() {
@Override
public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() - 1000L);
assertTrue(time < DateUtilities.now() + 2 * DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_OVERDUE);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// test due date in the past, but recently notified
task.setReminderLast(DateUtilities.now());
reminderService.setScheduler(new AlarmExpected() {
@Override
public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() + DateUtilities.ONE_HOUR);
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_OVERDUE);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
} }
@Test @Test
public void testMultipleReminders() { public void dontScheduleReminderForCompletedTask() {
// test due date in the future, enable random Task task = newTask(
final Task task = new Task(); with(ID, 1L),
task.setTitle("water"); with(DUE_TIME, newDateTime().plusDays(1)),
task.setDueDate(DateUtilities.now() + DateUtilities.ONE_WEEK); with(COMPLETION_TIME, newDateTime()),
task.setReminderFlags(Task.NOTIFY_AT_DEADLINE); with(REMINDERS, NOTIFY_AT_DEADLINE));
task.setReminderPeriod(DateUtilities.ONE_HOUR);
reminderService.setScheduler(new AlarmExpected() { service.scheduleAlarm(null, task);
@Override
public void createAlarm(Task task, long time, int type) { verify(jobs).cancel(1);
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now());
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_RANDOM);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// now set the due date in the past
task.setDueDate(DateUtilities.now() - DateUtilities.ONE_WEEK);
((AlarmExpected) reminderService.getScheduler()).alarmCreated = false;
reminderService.scheduleAlarm(taskDao, task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// now set the due date before the random
task.setDueDate(DateUtilities.now() + DateUtilities.ONE_HOUR);
reminderService.setScheduler(new AlarmExpected() {
@Override
public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertEquals((long) task.getDueDate(), time);
assertEquals(type, ReminderService.TYPE_DUE);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
} }
@Test @Test
public void testSnoozeReminders() { public void dontScheduleReminderForDeletedTask() {
thaw(); // TODO: get rid of this Task task = newTask(
with(ID, 1L),
// test due date and snooze in the future with(DUE_TIME, newDateTime().plusDays(1)),
final Task task = new Task(); with(DELETION_TIME, newDateTime()),
task.setTitle("spacemen"); with(REMINDERS, NOTIFY_AT_DEADLINE));
task.setDueDate(DateUtilities.now() + 5000L);
task.setReminderFlags(Task.NOTIFY_AT_DEADLINE); service.scheduleAlarm(null, task);
task.setReminderSnooze(DateUtilities.now() + DateUtilities.ONE_WEEK);
reminderService.setScheduler(new AlarmExpected() {
@Override
public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() + DateUtilities.ONE_WEEK - 1000L);
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_WEEK + 1000L);
assertEquals(type, ReminderService.TYPE_SNOOZE);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// snooze in the past
task.setReminderSnooze(DateUtilities.now() - DateUtilities.ONE_WEEK);
reminderService.setScheduler(new AlarmExpected() {
@Override
public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM)
return;
super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() - 1000L);
assertTrue(time < DateUtilities.now() + 5000L);
assertEquals(type, ReminderService.TYPE_DUE);
}
});
taskDao.save(task);
assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
}
// --- helper classes verify(jobs).cancel(1);
}
public class NoAlarmExpected implements AlarmScheduler { @Test
@Override public void dontScheduleDueDateReminderWhenAlreadyReminded() {
public void createAlarm(Task task, long time, int type) { DateTime now = newDateTime();
if(time == 0 || time == Long.MAX_VALUE) Task task = newTask(
return; with(ID, 1L),
fail("created alarm, no alarm expected (" + type + ": " + newDateTime(time)); with(DUE_TIME, now),
} with(REMINDER_LAST, now.plusSeconds(1)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
@Override service.scheduleAlarm(null, task);
public void clear() {
} verify(jobs).cancel(1);
} }
public class AlarmExpected implements AlarmScheduler { @Test
public boolean alarmCreated = false; public void snoozeOverridesAll() {
DateTime now = newDateTime();
Task task = newTask(
with(ID, 1L),
with(DUE_TIME, now),
with(SNOOZE_TIME, now.plusMonths(12)),
with(REMINDERS, NOTIFY_AT_DEADLINE | NOTIFY_AFTER_DEADLINE),
with(RANDOM_REMINDER_PERIOD, TimeUnit.HOURS.toMillis(1)));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancel(1);
order.verify(jobs).add(new Reminder(1, now.plusMonths(12).getMillis(), ReminderService.TYPE_SNOOZE));
}
@Override @Test
public void createAlarm(Task task, long time, int type) { public void ignoreLapsedSnoozeTime() {
alarmCreated = true; Task task = newTask(
} with(ID, 1L),
with(DUE_TIME, newDateTime()),
with(SNOOZE_TIME, newDateTime().minusMinutes(5)),
with(REMINDERS, NOTIFY_AT_DEADLINE));
service.scheduleAlarm(null, task);
InOrder order = inOrder(jobs);
order.verify(jobs).cancel(1);
order.verify(jobs).add(new Reminder(1, task.getDueDate(), ReminderService.TYPE_DUE));
@Override }
public void clear() {
} @Test
public void scheduleRandomReminder() {
freezeClock().thawAfter(new Snippet() {{
Task task = newTask(
with(ID, 1L),
with(RANDOM_REMINDER_PERIOD, TimeUnit.DAYS.toMillis(7)));
service.scheduleAlarm(null, task);
ArgumentCaptor<Reminder> captor = ArgumentCaptor.forClass(Reminder.class);
InOrder order = inOrder(jobs);
order.verify(jobs).cancel(1);
order.verify(jobs).add(captor.capture());
Reminder reminder = captor.getValue();
assertTrue(reminder.getTime() > currentTimeMillis());
assertTrue(reminder.getTime() < currentTimeMillis() + 1.2 * TimeUnit.DAYS.toMillis(7));
assertEquals(TYPE_RANDOM, reminder.getType());
}});
} }
// @Test
// public void testOverdue() {
// // test due date in the future
// reminderService.setScheduler(new AlarmExpected() {
// @Override
// public void createAlarm(Task task, long time, int type) {
// if (time == ReminderService.NO_ALARM)
// return;
// super.createAlarm(task, time, type);
// assertTrue(time > task.getDueDate());
// assertTrue(time < task.getDueDate() + DateUtilities.ONE_DAY);
// assertEquals(type, ReminderService.TYPE_OVERDUE);
// }
// });
// final Task task = new Task();
// task.setTitle("water");
// task.setDueDate(DateUtilities.now() + DateUtilities.ONE_DAY);
// task.setReminderFlags(Task.NOTIFY_AFTER_DEADLINE);
// taskDao.save(task);
//
// // test due date in the past
// task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY);
// reminderService.setScheduler(new AlarmExpected() {
// @Override
// public void createAlarm(Task task, long time, int type) {
// if (time == ReminderService.NO_ALARM)
// return;
// super.createAlarm(task, time, type);
// assertTrue(time > DateUtilities.now() - 1000L);
// assertTrue(time < DateUtilities.now() + 2 * DateUtilities.ONE_DAY);
// assertEquals(type, ReminderService.TYPE_OVERDUE);
// }
// });
// taskDao.save(task);
// assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
//
// // test due date in the past, but recently notified
// task.setReminderLast(DateUtilities.now());
// reminderService.setScheduler(new AlarmExpected() {
// @Override
// public void createAlarm(Task task, long time, int type) {
// if (time == ReminderService.NO_ALARM)
// return;
// super.createAlarm(task, time, type);
// assertTrue(time > DateUtilities.now() + DateUtilities.ONE_HOUR);
// assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
// assertEquals(type, ReminderService.TYPE_OVERDUE);
// }
// });
// taskDao.save(task);
// assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// }
//
// @Test
// public void testMultipleReminders() {
// // test due date in the future, enable random
// final Task task = new Task();
// task.setTitle("water");
// task.setDueDate(DateUtilities.now() + DateUtilities.ONE_WEEK);
// task.setReminderFlags(Task.NOTIFY_AT_DEADLINE);
// task.setReminderPeriod(DateUtilities.ONE_HOUR);
// reminderService.setScheduler(new AlarmExpected() {
// @Override
// public void createAlarm(Task task, long time, int type) {
// if (time == ReminderService.NO_ALARM)
// return;
// super.createAlarm(task, time, type);
// assertTrue(time > DateUtilities.now());
// assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
// assertEquals(type, ReminderService.TYPE_RANDOM);
// }
// });
// taskDao.save(task);
// assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
//
// // now set the due date in the past
// task.setDueDate(DateUtilities.now() - DateUtilities.ONE_WEEK);
// ((AlarmExpected) reminderService.getScheduler()).alarmCreated = false;
// reminderService.scheduleAlarm(taskDao, task);
// assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
//
// // now set the due date before the random
// task.setDueDate(DateUtilities.now() + DateUtilities.ONE_HOUR);
// reminderService.setScheduler(new AlarmExpected() {
// @Override
// public void createAlarm(Task task, long time, int type) {
// if (time == ReminderService.NO_ALARM)
// return;
// super.createAlarm(task, time, type);
// assertEquals((long) task.getDueDate(), time);
// assertEquals(type, ReminderService.TYPE_DUE);
// }
// });
// taskDao.save(task);
// assertTrue(((AlarmExpected) reminderService.getScheduler()).alarmCreated);
// }
} }

@ -2,9 +2,11 @@ package org.tasks.jobs;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.tasks.Freeze; import org.tasks.Freeze;
import org.tasks.Snippet; import org.tasks.Snippet;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
@ -14,11 +16,12 @@ import java.util.concurrent.TimeUnit;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_DUE; import static com.todoroo.astrid.reminders.ReminderService.TYPE_DUE;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.AdditionalAnswers.returnsFirstArg; import static org.mockito.AdditionalAnswers.returnsFirstArg;
import static org.mockito.Matchers.anyLong; import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@ -26,15 +29,23 @@ import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class JobQueueTest { public class JobQueueTest {
private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1); private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
private static final String TAG = "test";
private JobQueue<Reminder> queue; private JobQueue<Reminder> queue;
private JobManager jobManager;
private Preferences preferences; private Preferences preferences;
@Before @Before
public void before() { public void before() {
preferences = mock(Preferences.class); preferences = mock(Preferences.class);
when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg()); when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg());
queue = new JobQueue<>(preferences); jobManager = mock(JobManager.class);
queue = new JobQueue<>(preferences, jobManager, TAG);
}
@After
public void after() {
verifyNoMoreInteractions(jobManager);
} }
@Test @Test
@ -42,33 +53,44 @@ public class JobQueueTest {
queue.add(new Reminder(1, 1, 0)); queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 1, 0)); queue.add(new Reminder(2, 1, 0));
verify(jobManager).schedule(TAG, 1, true);
assertEquals(2, queue.size()); assertEquals(2, queue.size());
} }
@Test @Test
public void rescheduleForFirstJob() { public void rescheduleForFirstJob() {
assertTrue(queue.add(new Reminder(1, 1, 0))); queue.add(new Reminder(1, 1, 0));
verify(jobManager).schedule(TAG, 1, true);
} }
@Test @Test
public void dontRescheduleForLaterJobs() { public void dontRescheduleForLaterJobs() {
queue.add(new Reminder(1, 1, 0)); queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 2, 0));
assertFalse(queue.add(new Reminder(2, 2, 0))); verify(jobManager).schedule(TAG, 1, true);
} }
@Test @Test
public void rescheduleForNewerJob() { public void rescheduleForNewerJob() {
queue.add(new Reminder(1, 2, 0)); queue.add(new Reminder(1, 2, 0));
queue.add(new Reminder(1, 1, 0));
assertTrue(queue.add(new Reminder(1, 1, 0))); InOrder order = inOrder(jobManager);
order.verify(jobManager).schedule(TAG, 2, true);
order.verify(jobManager).schedule(TAG, 1, true);
} }
@Test @Test
public void rescheduleWhenCancelingOnlyJob() { public void rescheduleWhenCancelingOnlyJob() {
queue.add(new Reminder(1, 2, 0)); queue.add(new Reminder(1, 2, 0));
queue.cancel(1);
assertTrue(queue.cancel(1)); InOrder order = inOrder(jobManager);
order.verify(jobManager).schedule(TAG, 2, true);
order.verify(jobManager).cancel(TAG);
} }
@Test @Test
@ -76,7 +98,11 @@ public class JobQueueTest {
queue.add(new Reminder(1, 1, 0)); queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 2, 0)); queue.add(new Reminder(2, 2, 0));
assertTrue(queue.cancel(1)); queue.cancel(1);
InOrder order = inOrder(jobManager);
order.verify(jobManager).schedule(TAG, 1, true);
order.verify(jobManager).schedule(TAG, 2, true);
} }
@Test @Test
@ -84,7 +110,9 @@ public class JobQueueTest {
queue.add(new Reminder(1, 1, 0)); queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 2, 0)); queue.add(new Reminder(2, 2, 0));
assertFalse(queue.cancel(2)); queue.cancel(2);
verify(jobManager).schedule(TAG, 1, true);
} }
@Test @Test
@ -99,7 +127,7 @@ public class JobQueueTest {
when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L); when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L);
queue.add(new Reminder(1, 1, 1)); queue.add(new Reminder(1, 1, 1));
assertEquals(1234, queue.nextScheduledTime()); verify(jobManager).schedule(TAG, 1234, true);
} }
@Test @Test
@ -109,6 +137,8 @@ public class JobQueueTest {
queue.add(new Reminder(1, now, TYPE_DUE)); queue.add(new Reminder(1, now, TYPE_DUE));
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
verify(jobManager).schedule(TAG, now, true);
Freeze.freezeAt(now).thawAfter(new Snippet() {{ Freeze.freezeAt(now).thawAfter(new Snippet() {{
assertEquals( assertEquals(
singletonList(new Reminder(1, now, TYPE_DUE)), singletonList(new Reminder(1, now, TYPE_DUE)),
@ -123,6 +153,8 @@ public class JobQueueTest {
queue.add(new Reminder(1, now, TYPE_DUE)); queue.add(new Reminder(1, now, TYPE_DUE));
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)); queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
verify(jobManager).schedule(TAG, now, true);
Freeze.freezeAt(now).thawAfter(new Snippet() {{ Freeze.freezeAt(now).thawAfter(new Snippet() {{
queue.removeOverdueJobs(); queue.removeOverdueJobs();
}}); }});
@ -131,4 +163,26 @@ public class JobQueueTest {
singletonList(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)), singletonList(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)),
queue.getJobs()); queue.getJobs());
} }
@Test
public void clearShouldCancelExisting() {
queue.add(new Reminder(1, 1, 0));
queue.clear();
InOrder order = inOrder(jobManager);
order.verify(jobManager).schedule(TAG, 1, true);
order.verify(jobManager).cancel(TAG);
assertEquals(0, queue.size());
}
@Test
public void ignoreInvalidCancel() {
long now = currentTimeMillis();
queue.add(new Reminder(1, now, TYPE_DUE));
queue.cancel(2);
verify(jobManager).schedule(TAG, now, true);
}
} }

@ -1,5 +1,6 @@
package org.tasks.makers; package org.tasks.makers;
import com.google.common.base.Strings;
import com.natpryce.makeiteasy.Instantiator; import com.natpryce.makeiteasy.Instantiator;
import com.natpryce.makeiteasy.Property; import com.natpryce.makeiteasy.Property;
import com.natpryce.makeiteasy.PropertyValue; import com.natpryce.makeiteasy.PropertyValue;
@ -9,15 +10,21 @@ import org.tasks.time.DateTime;
import static com.natpryce.makeiteasy.Property.newProperty; import static com.natpryce.makeiteasy.Property.newProperty;
import static org.tasks.makers.Maker.make; import static org.tasks.makers.Maker.make;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class TaskMaker { public class TaskMaker {
public static Property<Task, Long> ID = newProperty(); public static Property<Task, Long> ID = newProperty();
public static Property<Task, String> TITLE = newProperty();
public static Property<Task, DateTime> DUE_DATE = newProperty(); public static Property<Task, DateTime> DUE_DATE = newProperty();
public static Property<Task, DateTime> DUE_TIME = newProperty(); public static Property<Task, DateTime> DUE_TIME = newProperty();
public static Property<Task, DateTime> REMINDER_LAST = newProperty(); public static Property<Task, DateTime> REMINDER_LAST = newProperty();
public static Property<Task, Long> RANDOM_REMINDER_PERIOD = newProperty();
public static Property<Task, Integer> HIDE_TYPE = newProperty(); public static Property<Task, Integer> HIDE_TYPE = newProperty();
public static Property<Task, Integer> REMINDERS = newProperty(); public static Property<Task, Integer> REMINDERS = newProperty();
public static Property<Task, DateTime> COMPLETION_TIME = newProperty();
public static Property<Task, DateTime> DELETION_TIME = newProperty();
public static Property<Task, DateTime> SNOOZE_TIME = newProperty();
@SafeVarargs @SafeVarargs
public static Task newTask(PropertyValue<? super Task, ?>... properties) { public static Task newTask(PropertyValue<? super Task, ?>... properties) {
@ -27,6 +34,11 @@ public class TaskMaker {
private static final Instantiator<Task> instantiator = lookup -> { private static final Instantiator<Task> instantiator = lookup -> {
Task task = new Task(); Task task = new Task();
String title = lookup.valueOf(TITLE, (String) null);
if (!Strings.isNullOrEmpty(title)) {
task.setTitle(title);
}
long id = lookup.valueOf(ID, Task.NO_ID); long id = lookup.valueOf(ID, Task.NO_ID);
if (id != Task.NO_ID) { if (id != Task.NO_ID) {
task.setId(id); task.setId(id);
@ -42,6 +54,21 @@ public class TaskMaker {
task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueTime.getMillis())); task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, dueTime.getMillis()));
} }
DateTime completionTime = lookup.valueOf(COMPLETION_TIME, (DateTime) null);
if (completionTime != null) {
task.setCompletionDate(completionTime.getMillis());
}
DateTime deletedTime = lookup.valueOf(DELETION_TIME, (DateTime) null);
if (deletedTime != null) {
task.setDeletionDate(deletedTime.getMillis());
}
DateTime snoozeTime = lookup.valueOf(SNOOZE_TIME, (DateTime) null);
if (snoozeTime != null) {
task.setReminderSnooze(snoozeTime.getMillis());
}
int hideType = lookup.valueOf(HIDE_TYPE, -1); int hideType = lookup.valueOf(HIDE_TYPE, -1);
if (hideType >= 0) { if (hideType >= 0) {
task.setHideUntil(task.createHideUntil(hideType, 0)); task.setHideUntil(task.createHideUntil(hideType, 0));
@ -57,6 +84,13 @@ public class TaskMaker {
task.setReminderLast(reminderLast.getMillis()); task.setReminderLast(reminderLast.getMillis());
} }
long randomReminderPeriod = lookup.valueOf(RANDOM_REMINDER_PERIOD, 0L);
if (randomReminderPeriod > 0) {
task.setReminderPeriod(randomReminderPeriod);
}
task.setCreationDate(currentTimeMillis());
return task; return task;
}; };
} }

@ -48,13 +48,11 @@ public class AlarmService {
private final JobQueue<Alarm> jobs; private final JobQueue<Alarm> jobs;
private final MetadataDao metadataDao; private final MetadataDao metadataDao;
private final JobManager jobManager;
@Inject @Inject
public AlarmService(MetadataDao metadataDao, JobManager jobManager, Preferences preferences) { public AlarmService(MetadataDao metadataDao, JobManager jobManager, Preferences preferences) {
this.metadataDao = metadataDao; this.metadataDao = metadataDao;
this.jobManager = jobManager; jobs = JobQueue.newAlarmQueue(preferences, jobManager);
jobs = new JobQueue<>(preferences);
} }
public void getAlarms(long taskId, Callback<Metadata> callback) { public void getAlarms(long taskId, Callback<Metadata> callback) {
@ -77,7 +75,7 @@ public class AlarmService {
metadata.add(item); metadata.add(item);
} }
boolean changed = synchronizeMetadata(taskId, metadata, m -> cancelAlarm(m.getId())); boolean changed = synchronizeMetadata(taskId, metadata, m -> jobs.cancel(m.getId()));
if(changed) { if(changed) {
scheduleAlarms(taskId); scheduleAlarms(taskId);
@ -103,7 +101,6 @@ public class AlarmService {
public void clear() { public void clear() {
jobs.clear(); jobs.clear();
jobManager.cancelAlarms();
} }
/** /**
@ -131,17 +128,9 @@ public class AlarmService {
Alarm alarm = new Alarm(metadata); Alarm alarm = new Alarm(metadata);
long time = alarm.getTime(); long time = alarm.getTime();
if(time == 0 || time == NO_ALARM) { if(time == 0 || time == NO_ALARM) {
cancelAlarm(alarm.getId()); jobs.cancel(alarm.getId());
} else { } else {
if (jobs.add(alarm)) { jobs.add(alarm);
scheduleNext(true);
}
}
}
private void cancelAlarm(Long alarmId) {
if (jobs.cancel(alarmId)) {
scheduleNext(true);
} }
} }
@ -197,13 +186,7 @@ public class AlarmService {
} }
public void scheduleNextJob() { public void scheduleNextJob() {
scheduleNext(false); jobs.scheduleNext();
}
private void scheduleNext(boolean cancelCurrent) {
if (!jobs.isEmpty()) {
jobManager.scheduleAlarm(jobs.nextScheduledTime(), cancelCurrent);
}
} }
public List<Alarm> removePastDueAlarms() { public List<Alarm> removePastDueAlarms() {

@ -1,9 +0,0 @@
package com.todoroo.astrid.reminders;
import com.todoroo.astrid.data.Task;
public interface AlarmScheduler {
void createAlarm(Task task, long time, int type);
void clear();
}

@ -1,74 +0,0 @@
package com.todoroo.astrid.reminders;
import com.todoroo.astrid.data.Task;
import org.tasks.injection.ApplicationScope;
import org.tasks.jobs.JobManager;
import org.tasks.jobs.JobQueue;
import org.tasks.jobs.Reminder;
import org.tasks.preferences.Preferences;
import java.util.List;
import javax.inject.Inject;
import static com.todoroo.astrid.reminders.ReminderService.NO_ALARM;
@ApplicationScope
public class ReminderAlarmScheduler implements AlarmScheduler {
private final JobQueue<Reminder> jobs;
private final JobManager jobManager;
@Inject
public ReminderAlarmScheduler(JobManager jobManager, Preferences preferences) {
this.jobManager = jobManager;
jobs = new JobQueue<>(preferences);
}
/**
* Create an alarm for the given task at the given type
*/
@Override
public void createAlarm(Task task, long time, int type) {
long taskId = task.getId();
if(taskId == Task.NO_ID) {
return;
}
if (time == 0 || time == NO_ALARM) {
if (jobs.cancel(taskId)) {
scheduleNext(true);
}
} else {
Reminder reminder = new Reminder(taskId, time, type);
if (jobs.add(reminder)) {
scheduleNext(true);
}
}
}
@Override
public void clear() {
jobs.clear();
jobManager.cancelReminders();
}
public void scheduleNextJob() {
scheduleNext(false);
}
private void scheduleNext(boolean cancelCurrent) {
if (jobs.isEmpty()) {
if (cancelCurrent) {
jobManager.cancelReminders();
}
} else {
jobManager.scheduleReminder(jobs.nextScheduledTime(), cancelCurrent);
}
}
public List<Reminder> removePastReminders() {
return jobs.removeOverdueJobs();
}
}

@ -16,13 +16,10 @@ import android.preference.PreferenceManager;
import android.provider.Settings; import android.provider.Settings;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.todoroo.astrid.alarms.AlarmService;
import org.tasks.R; import org.tasks.R;
import org.tasks.activities.ColorPickerActivity; import org.tasks.activities.ColorPickerActivity;
import org.tasks.activities.TimePickerActivity; import org.tasks.activities.TimePickerActivity;
import org.tasks.dialogs.ColorPickerDialog; import org.tasks.dialogs.ColorPickerDialog;
import org.tasks.dialogs.DialogBuilder;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingPreferenceActivity; import org.tasks.injection.InjectingPreferenceActivity;
import org.tasks.preferences.ActivityPermissionRequestor; import org.tasks.preferences.ActivityPermissionRequestor;
@ -52,10 +49,8 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
@Inject Device device; @Inject Device device;
@Inject ActivityPermissionRequestor permissionRequestor; @Inject ActivityPermissionRequestor permissionRequestor;
@Inject PermissionChecker permissionChecker; @Inject PermissionChecker permissionChecker;
@Inject DialogBuilder dialogBuilder;
@Inject Preferences preferences; @Inject Preferences preferences;
@Inject ThemeCache themeCache; @Inject ThemeCache themeCache;
@Inject AlarmService alarmService;
private CheckBoxPreference fieldMissedCalls; private CheckBoxPreference fieldMissedCalls;

@ -15,21 +15,19 @@ import com.todoroo.astrid.data.Task;
import org.tasks.R; import org.tasks.R;
import org.tasks.injection.ApplicationScope; import org.tasks.injection.ApplicationScope;
import org.tasks.jobs.JobManager;
import org.tasks.jobs.JobQueue;
import org.tasks.jobs.Reminder;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.util.List;
import java.util.Random; import java.util.Random;
import javax.inject.Inject; import javax.inject.Inject;
import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.date.DateTimeUtils.newDateTime;
/**
* Data service for reminders
*
* @author Tim Su <tim@todoroo.com>
*
*/
@ApplicationScope @ApplicationScope
public final class ReminderService { public final class ReminderService {
@ -60,38 +58,36 @@ public final class ReminderService {
public static final int TYPE_ALARM = 4; public static final int TYPE_ALARM = 4;
private static final Random random = new Random(); private static final Random random = new Random();
private static final long NO_ALARM = Long.MAX_VALUE;
// --- instance variables private final JobQueue<Reminder> jobs;
private final Preferences preferences;
private AlarmScheduler scheduler;
private long now = -1; // For tracking when reminders might be scheduled all at once private long now = -1; // For tracking when reminders might be scheduled all at once
private final Preferences preferences;
@Inject @Inject
ReminderService(Preferences preferences, ReminderAlarmScheduler scheduler) { ReminderService(Preferences preferences, JobManager jobManager) {
this(preferences, JobQueue.newReminderQueue(preferences, jobManager));
}
ReminderService(Preferences preferences, JobQueue<Reminder> jobs) {
this.preferences = preferences; this.preferences = preferences;
this.scheduler = scheduler; this.jobs = jobs;
} }
private static final int MILLIS_PER_HOUR = 60 * 60 * 1000; private static final int MILLIS_PER_HOUR = 60 * 60 * 1000;
// --- reminder scheduling logic
/**
* Schedules all alarms
*/
public void scheduleAllAlarms(TaskDao taskDao) { public void scheduleAllAlarms(TaskDao taskDao) {
now = DateUtilities.now(); // Before mass scheduling, initialize now variable now = DateUtilities.now(); // Before mass scheduling, initialize now variable
Query query = Query.select(NOTIFICATION_PROPERTIES).where(Criterion.and( Query query = Query.select(NOTIFICATION_PROPERTIES).where(Criterion.and(
TaskCriteria.isActive(), TaskCriteria.isActive(),
Criterion.or(Task.REMINDER_FLAGS.gt(0), Task.REMINDER_PERIOD.gt(0)))); Criterion.or(Task.REMINDER_FLAGS.gt(0), Task.REMINDER_PERIOD.gt(0))));
taskDao.forEach(query, task -> scheduleAlarm(task, null)); taskDao.forEach(query, task -> scheduleAlarm(null, task));
now = -1; // Signal done with now variable now = -1; // Signal done with now variable
} }
public void clear() { public void clear() {
scheduler.clear(); jobs.clear();
} }
private long getNowValue() { private long getNowValue() {
@ -99,29 +95,25 @@ public final class ReminderService {
return (now == -1 ? DateUtilities.now() : now); return (now == -1 ? DateUtilities.now() : now);
} }
public static final long NO_ALARM = Long.MAX_VALUE; public List<Reminder> getPastReminders() {
return jobs.removeOverdueJobs();
/**
* Schedules alarms for a single task
*/
public void scheduleAlarm(TaskDao taskDao, Task task) {
scheduleAlarm(task, taskDao);
} }
private void clearAllAlarms(Task task) { public void scheduleNextJob() {
scheduler.createAlarm(task, NO_ALARM, 0); jobs.scheduleNext();
} }
private void scheduleAlarm(Task task, TaskDao taskDao) { public void scheduleAlarm(TaskDao taskDao, Task task) {
if(task == null || !task.isSaved()) { if(task == null || !task.isSaved()) {
return; return;
} }
// read data if necessary // read data if necessary
long taskId = task.getId();
if(taskDao != null) { if(taskDao != null) {
for(Property<?> property : NOTIFICATION_PROPERTIES) { for(Property<?> property : NOTIFICATION_PROPERTIES) {
if(!task.containsValue(property)) { if(!task.containsValue(property)) {
task = taskDao.fetch(task.getId(), NOTIFICATION_PROPERTIES); task = taskDao.fetch(taskId, NOTIFICATION_PROPERTIES);
if(task == null) { if(task == null) {
return; return;
} }
@ -132,7 +124,8 @@ public final class ReminderService {
// Make sure no alarms are scheduled other than the next one. When that one is shown, it // Make sure no alarms are scheduled other than the next one. When that one is shown, it
// will schedule the next one after it, and so on and so forth. // will schedule the next one after it, and so on and so forth.
clearAllAlarms(task); jobs.cancel(taskId);
if(task.isCompleted() || task.isDeleted()) { if(task.isCompleted() || task.isDeleted()) {
return; return;
} }
@ -164,15 +157,13 @@ public final class ReminderService {
// snooze trumps all // snooze trumps all
if(whenSnooze != NO_ALARM) { if(whenSnooze != NO_ALARM) {
scheduler.createAlarm(task, whenSnooze, TYPE_SNOOZE); jobs.add(new Reminder(taskId, whenSnooze, TYPE_SNOOZE));
} else if(whenRandom < whenDueDate && whenRandom < whenOverdue) { } else if(whenRandom < whenDueDate && whenRandom < whenOverdue) {
scheduler.createAlarm(task, whenRandom, TYPE_RANDOM); jobs.add(new Reminder(taskId, whenRandom, TYPE_RANDOM));
} else if(whenDueDate < whenOverdue) { } else if(whenDueDate < whenOverdue) {
scheduler.createAlarm(task, whenDueDate, TYPE_DUE); jobs.add(new Reminder(taskId, whenDueDate, TYPE_DUE));
} else if(whenOverdue != NO_ALARM) { } else if(whenOverdue != NO_ALARM) {
scheduler.createAlarm(task, whenOverdue, TYPE_OVERDUE); jobs.add(new Reminder(taskId, whenOverdue, TYPE_OVERDUE));
} else {
scheduler.createAlarm(task, 0, 0);
} }
} }
@ -238,7 +229,7 @@ public final class ReminderService {
* If the date was indicated to not have a due time, we read from * If the date was indicated to not have a due time, we read from
* preferences and assign a time. * preferences and assign a time.
*/ */
long calculateNextDueDateReminder(Task task) { private long calculateNextDueDateReminder(Task task) {
if(task.hasDueDate() && task.isNotifyAtDeadline()) { if(task.hasDueDate() && task.isNotifyAtDeadline()) {
long dueDate = task.getDueDate(); long dueDate = task.getDueDate();
long lastReminder = task.getReminderLast(); long lastReminder = task.getReminderLast();
@ -285,14 +276,4 @@ public final class ReminderService {
} }
return NO_ALARM; return NO_ALARM;
} }
// --- alarm manager alarm creation
public void setScheduler(AlarmScheduler scheduler) {
this.scheduler = scheduler;
}
public AlarmScheduler getScheduler() {
return scheduler;
}
} }

@ -6,7 +6,7 @@ import com.evernote.android.job.Job;
import com.todoroo.astrid.alarms.AlarmService; import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.backup.TasksXmlExporter; import com.todoroo.astrid.backup.TasksXmlExporter;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.reminders.ReminderAlarmScheduler; import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.Broadcaster; import org.tasks.Broadcaster;
import org.tasks.Notifier; import org.tasks.Notifier;
@ -29,13 +29,13 @@ public class JobCreator implements com.evernote.android.job.JobCreator {
private final RefreshScheduler refreshScheduler; private final RefreshScheduler refreshScheduler;
private final AlarmService alarmService; private final AlarmService alarmService;
private final TaskDao taskDao; private final TaskDao taskDao;
private final ReminderAlarmScheduler reminderAlarmScheduler; private final ReminderService reminderService;
@Inject @Inject
public JobCreator(@ForApplication Context context, Notifier notifier, JobManager jobManager, public JobCreator(@ForApplication Context context, Notifier notifier, JobManager jobManager,
Broadcaster broadcaster, TasksXmlExporter tasksXmlExporter, Broadcaster broadcaster, TasksXmlExporter tasksXmlExporter,
Preferences preferences, RefreshScheduler refreshScheduler, Preferences preferences, RefreshScheduler refreshScheduler,
AlarmService alarmService, TaskDao taskDao, ReminderAlarmScheduler reminderAlarmScheduler) { AlarmService alarmService, TaskDao taskDao, ReminderService reminderService) {
this.context = context; this.context = context;
this.notifier = notifier; this.notifier = notifier;
this.jobManager = jobManager; this.jobManager = jobManager;
@ -45,14 +45,14 @@ public class JobCreator implements com.evernote.android.job.JobCreator {
this.refreshScheduler = refreshScheduler; this.refreshScheduler = refreshScheduler;
this.alarmService = alarmService; this.alarmService = alarmService;
this.taskDao = taskDao; this.taskDao = taskDao;
this.reminderAlarmScheduler = reminderAlarmScheduler; this.reminderService = reminderService;
} }
@Override @Override
public Job create(String tag) { public Job create(String tag) {
switch (tag) { switch (tag) {
case ReminderJob.TAG: case ReminderJob.TAG:
return new ReminderJob(preferences, reminderAlarmScheduler, notifier); return new ReminderJob(preferences, reminderService, notifier);
case AlarmJob.TAG: case AlarmJob.TAG:
return new AlarmJob(preferences, alarmService, notifier, taskDao); return new AlarmJob(preferences, alarmService, notifier, taskDao);
case RefreshJob.TAG: case RefreshJob.TAG:

@ -28,16 +28,8 @@ public class JobManager {
jobManager.addJobCreator(jobCreator); jobManager.addJobCreator(jobCreator);
} }
public void scheduleAlarm(long time, boolean cancelCurrent) { public void schedule(String tag, long time, boolean cancelCurrent) {
new JobRequest.Builder(AlarmJob.TAG) new JobRequest.Builder(tag)
.setExact(Math.max(time - currentTimeMillis(), 5000))
.setUpdateCurrent(cancelCurrent)
.build()
.schedule();
}
public void scheduleReminder(long time, boolean cancelCurrent) {
new JobRequest.Builder(ReminderJob.TAG)
.setExact(Math.max(time - currentTimeMillis(), 5000)) .setExact(Math.max(time - currentTimeMillis(), 5000))
.setUpdateCurrent(cancelCurrent) .setUpdateCurrent(cancelCurrent)
.build() .build()
@ -69,15 +61,11 @@ public class JobManager {
.schedule(); .schedule();
} }
public void cancelAlarms() { public void cancel(String tag) {
jobManager.cancelAllForTag(AlarmJob.TAG); jobManager.cancelAllForTag(tag);
} }
public void cancelRefreshes() { public void cancelRefreshes() {
jobManager.cancelAllForTag(RefreshJob.TAG); jobManager.cancelAllForTag(RefreshJob.TAG);
} }
public void cancelReminders() {
jobManager.cancelAllForTag(ReminderJob.TAG);
}
} }

@ -17,26 +17,37 @@ import static org.tasks.time.DateTimeUtils.currentTimeMillis;
public class JobQueue<T extends JobQueueEntry> { public class JobQueue<T extends JobQueueEntry> {
private final TreeMultimap<Long, T> jobs = TreeMultimap.create(Ordering.natural(), (l, r) -> Longs.compare(l.getId(), r.getId())); private final TreeMultimap<Long, T> jobs = TreeMultimap.create(Ordering.natural(), (l, r) -> Longs.compare(l.getId(), r.getId()));
private final Preferences preferences; private final Preferences preferences;
private final JobManager jobManager;
private final String tag;
public JobQueue(Preferences preferences) { public static JobQueue<Reminder> newReminderQueue(Preferences preferences, JobManager jobManager) {
this.preferences = preferences; return new JobQueue<>(preferences, jobManager, ReminderJob.TAG);
} }
public boolean add(T entry) { public static JobQueue<Alarm> newAlarmQueue(Preferences preferences, JobManager jobManager) {
boolean result = jobs.isEmpty() || entry.getTime() < firstTime(); return new JobQueue<>(preferences, jobManager, AlarmJob.TAG);
jobs.put(entry.getTime(), entry); }
return result;
JobQueue(Preferences preferences, JobManager jobManager, String tag) {
this.preferences = preferences;
this.jobManager = jobManager;
this.tag = tag;
} }
public boolean isEmpty() { public void add(T entry) {
return jobs.isEmpty(); boolean reschedule = jobs.isEmpty() || entry.getTime() < firstTime();
jobs.put(entry.getTime(), entry);
if (reschedule) {
scheduleNext(true);
}
} }
public void clear() { public void clear() {
jobs.clear(); jobs.clear();
jobManager.cancel(tag);
} }
public boolean cancel(long id) { public void cancel(long id) {
boolean reschedule = false; boolean reschedule = false;
long firstTime = firstTime(); long firstTime = firstTime();
List<T> existing = newArrayList(filter(jobs.values(), r -> r.getId() == id)); List<T> existing = newArrayList(filter(jobs.values(), r -> r.getId() == id));
@ -44,11 +55,9 @@ public class JobQueue<T extends JobQueueEntry> {
reschedule |= entry.getTime() == firstTime; reschedule |= entry.getTime() == firstTime;
jobs.remove(entry.getTime(), entry); jobs.remove(entry.getTime(), entry);
} }
return reschedule; if (reschedule) {
} scheduleNext(true);
}
List<T> getJobs() {
return ImmutableList.copyOf(jobs.values());
} }
public List<T> removeOverdueJobs() { public List<T> removeOverdueJobs() {
@ -60,16 +69,34 @@ public class JobQueue<T extends JobQueueEntry> {
return result; return result;
} }
public int size() { public void scheduleNext() {
return jobs.size(); scheduleNext(false);
}
private void scheduleNext(boolean cancelCurrent) {
if (jobs.isEmpty()) {
if (cancelCurrent) {
jobManager.cancel(tag);
}
} else {
jobManager.schedule(tag, nextScheduledTime(), cancelCurrent);
}
} }
private long firstTime() { private long firstTime() {
return jobs.isEmpty() ? 0 : jobs.asMap().firstKey(); return jobs.isEmpty() ? 0 : jobs.asMap().firstKey();
} }
public long nextScheduledTime() { long nextScheduledTime() {
long next = firstTime(); long next = firstTime();
return next > 0 ? preferences.adjustForQuietHours(next) : 0; return next > 0 ? preferences.adjustForQuietHours(next) : 0;
} }
int size() {
return jobs.size();
}
List<T> getJobs() {
return ImmutableList.copyOf(jobs.values());
}
} }

@ -3,7 +3,7 @@ package org.tasks.jobs;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import com.evernote.android.job.Job; import com.evernote.android.job.Job;
import com.todoroo.astrid.reminders.ReminderAlarmScheduler; import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.Notifier; import org.tasks.Notifier;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
@ -13,12 +13,12 @@ public class ReminderJob extends Job {
public static final String TAG = "job_reminder"; public static final String TAG = "job_reminder";
private final Preferences preferences; private final Preferences preferences;
private final ReminderAlarmScheduler reminderAlarmScheduler; private final ReminderService reminderService;
private final Notifier notifier; private final Notifier notifier;
public ReminderJob(Preferences preferences, ReminderAlarmScheduler reminderAlarmScheduler, Notifier notifier) { public ReminderJob(Preferences preferences, ReminderService reminderService, Notifier notifier) {
this.preferences = preferences; this.preferences = preferences;
this.reminderAlarmScheduler = reminderAlarmScheduler; this.reminderService = reminderService;
this.notifier = notifier; this.notifier = notifier;
} }
@ -27,13 +27,13 @@ public class ReminderJob extends Job {
protected Result onRunJob(Params params) { protected Result onRunJob(Params params) {
try { try {
if (!preferences.isCurrentlyQuietHours()) { if (!preferences.isCurrentlyQuietHours()) {
for (Reminder reminder : reminderAlarmScheduler.removePastReminders()) { for (Reminder reminder : reminderService.getPastReminders()) {
notifier.triggerTaskNotification(reminder.getId(), reminder.getType()); notifier.triggerTaskNotification(reminder.getId(), reminder.getType());
} }
} }
return Result.SUCCESS; return Result.SUCCESS;
} finally { } finally {
reminderAlarmScheduler.scheduleNextJob(); reminderService.scheduleNextJob();
} }
} }
} }

Loading…
Cancel
Save