Use android-job for most scheduling

pull/127/merge
Alex Baker 8 years ago
parent b485b139c5
commit c02164ad7a

@ -134,6 +134,7 @@ dependencies {
compile 'com.google.android.apps.dashclock:dashclock-api:2.0.0' compile 'com.google.android.apps.dashclock:dashclock-api:2.0.0'
compile 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2' compile 'com.twofortyfouram:android-plugin-api-for-locale:1.0.2'
compile 'com.bignerdranch.android:recyclerview-multiselect:0.2' compile 'com.bignerdranch.android:recyclerview-multiselect:0.2'
compile 'com.evernote:android-job:1.1.8'
compile ('com.rubiconproject.oss:jchronic:0.2.6') { compile ('com.rubiconproject.oss:jchronic:0.2.6') {
transitive = false transitive = false
} }

10
proguard.pro vendored

@ -37,3 +37,13 @@
# https://github.com/facebook/stetho/blob/2807d4248c6fa06cdd3626b6afb9bfc42ba50d55/stetho/proguard-consumer.pro # https://github.com/facebook/stetho/blob/2807d4248c6fa06cdd3626b6afb9bfc42ba50d55/stetho/proguard-consumer.pro
-keep class com.facebook.stetho.** { *; } -keep class com.facebook.stetho.** { *; }
-dontwarn com.facebook.stetho.** -dontwarn com.facebook.stetho.**
# https://github.com/evernote/android-job/blob/7f81ac43d0b161f4f0bed1e02c2455a3cda57041/library/proguard.txt
-dontwarn com.evernote.android.job.gcm.**
-dontwarn com.evernote.android.job.util.GcmAvailableHelper
-keep public class com.evernote.android.job.v21.PlatformJobService
-keep public class com.evernote.android.job.v14.PlatformAlarmService
-keep public class com.evernote.android.job.v14.PlatformAlarmReceiver
-keep public class com.evernote.android.job.JobBootReceiver
-keep public class com.evernote.android.job.JobRescheduleService

@ -12,8 +12,6 @@ import org.tasks.receivers.BootCompletedReceiver;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.ListNotificationReceiver;
import org.tasks.receivers.MyPackageReplacedReceiver; import org.tasks.receivers.MyPackageReplacedReceiver;
import org.tasks.receivers.RefreshReceiver;
import org.tasks.receivers.TaskNotificationReceiver;
import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.receivers.TeslaUnreadReceiver;
import org.tasks.widget.TasksWidget; import org.tasks.widget.TasksWidget;
@ -37,10 +35,6 @@ public interface BroadcastComponent {
void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); void inject(MyPackageReplacedReceiver myPackageReplacedReceiver);
void inject(RefreshReceiver refreshReceiver);
void inject(TaskNotificationReceiver taskNotificationReceiver);
void inject(CompleteTaskReceiver completeTaskReceiver); void inject(CompleteTaskReceiver completeTaskReceiver);
void inject(ListNotificationReceiver listNotificationReceiver); void inject(ListNotificationReceiver listNotificationReceiver);

@ -10,7 +10,6 @@ import org.junit.runner.RunWith;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getTargetContext; import static android.support.test.InstrumentationRegistry.getTargetContext;
import static com.natpryce.makeiteasy.MakeItEasy.with; import static com.natpryce.makeiteasy.MakeItEasy.with;
import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE; import static com.todoroo.astrid.data.Task.NOTIFY_AT_DEADLINE;
@ -30,7 +29,7 @@ public class NotifyAtDeadlineTest {
@Before @Before
public void setUp() { public void setUp() {
Preferences preferences = new Preferences(getTargetContext(), null); Preferences preferences = new Preferences(getTargetContext(), null);
reminderService = new ReminderService(getContext(), preferences, null); reminderService = new ReminderService(preferences, null);
} }
@Test @Test

@ -0,0 +1,139 @@
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);
}});
}
}

@ -5,13 +5,11 @@
*/ */
package com.todoroo.astrid.reminders; package com.todoroo.astrid.reminders;
import android.content.Context;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DateUtilities;
import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.reminders.ReminderService.AlarmScheduler;
import org.junit.After; import org.junit.After;
import org.junit.Test; import org.junit.Test;
@ -21,8 +19,6 @@ import org.tasks.injection.TestComponent;
import javax.inject.Inject; import javax.inject.Inject;
import static android.support.test.InstrumentationRegistry.getContext;
import static android.support.test.InstrumentationRegistry.getTargetContext;
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 junit.framework.Assert.fail;
@ -68,10 +64,10 @@ public class ReminderServiceTest extends InjectingTestCase {
public void testDueDates() { public void testDueDates() {
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getTargetContext(), task, time, type); super.createAlarm(task, time, type);
assertEquals((long) task.getDueDate(), time); assertEquals((long) task.getDueDate(), time);
assertEquals(type, ReminderService.TYPE_DUE); assertEquals(type, ReminderService.TYPE_DUE);
} }
@ -98,10 +94,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setReminderPeriod(DateUtilities.ONE_WEEK); task.setReminderPeriod(DateUtilities.ONE_WEEK);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now()); assertTrue(time > DateUtilities.now());
assertTrue(time < DateUtilities.now() + 1.2 * DateUtilities.ONE_WEEK); assertTrue(time < DateUtilities.now() + 1.2 * DateUtilities.ONE_WEEK);
assertEquals(type, ReminderService.TYPE_RANDOM); assertEquals(type, ReminderService.TYPE_RANDOM);
@ -116,10 +112,10 @@ public class ReminderServiceTest extends InjectingTestCase {
// test due date in the future // test due date in the future
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > task.getDueDate()); assertTrue(time > task.getDueDate());
assertTrue(time < task.getDueDate() + DateUtilities.ONE_DAY); assertTrue(time < task.getDueDate() + DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_OVERDUE); assertEquals(type, ReminderService.TYPE_OVERDUE);
@ -135,10 +131,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY); task.setDueDate(DateUtilities.now() - DateUtilities.ONE_DAY);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() - 1000L); assertTrue(time > DateUtilities.now() - 1000L);
assertTrue(time < DateUtilities.now() + 2 * DateUtilities.ONE_DAY); assertTrue(time < DateUtilities.now() + 2 * DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_OVERDUE); assertEquals(type, ReminderService.TYPE_OVERDUE);
@ -151,10 +147,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setReminderLast(DateUtilities.now()); task.setReminderLast(DateUtilities.now());
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() + DateUtilities.ONE_HOUR); assertTrue(time > DateUtilities.now() + DateUtilities.ONE_HOUR);
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY); assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_OVERDUE); assertEquals(type, ReminderService.TYPE_OVERDUE);
@ -174,10 +170,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setReminderPeriod(DateUtilities.ONE_HOUR); task.setReminderPeriod(DateUtilities.ONE_HOUR);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now()); assertTrue(time > DateUtilities.now());
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY); assertTrue(time < DateUtilities.now() + DateUtilities.ONE_DAY);
assertEquals(type, ReminderService.TYPE_RANDOM); assertEquals(type, ReminderService.TYPE_RANDOM);
@ -196,10 +192,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setDueDate(DateUtilities.now() + DateUtilities.ONE_HOUR); task.setDueDate(DateUtilities.now() + DateUtilities.ONE_HOUR);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertEquals((long) task.getDueDate(), time); assertEquals((long) task.getDueDate(), time);
assertEquals(type, ReminderService.TYPE_DUE); assertEquals(type, ReminderService.TYPE_DUE);
} }
@ -220,10 +216,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setReminderSnooze(DateUtilities.now() + DateUtilities.ONE_WEEK); task.setReminderSnooze(DateUtilities.now() + DateUtilities.ONE_WEEK);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() + DateUtilities.ONE_WEEK - 1000L); assertTrue(time > DateUtilities.now() + DateUtilities.ONE_WEEK - 1000L);
assertTrue(time < DateUtilities.now() + DateUtilities.ONE_WEEK + 1000L); assertTrue(time < DateUtilities.now() + DateUtilities.ONE_WEEK + 1000L);
assertEquals(type, ReminderService.TYPE_SNOOZE); assertEquals(type, ReminderService.TYPE_SNOOZE);
@ -236,10 +232,10 @@ public class ReminderServiceTest extends InjectingTestCase {
task.setReminderSnooze(DateUtilities.now() - DateUtilities.ONE_WEEK); task.setReminderSnooze(DateUtilities.now() - DateUtilities.ONE_WEEK);
reminderService.setScheduler(new AlarmExpected() { reminderService.setScheduler(new AlarmExpected() {
@Override @Override
public void createAlarm(Context context, Task task, long time, int type) { public void createAlarm(Task task, long time, int type) {
if (time == ReminderService.NO_ALARM) if (time == ReminderService.NO_ALARM)
return; return;
super.createAlarm(getContext(), task, time, type); super.createAlarm(task, time, type);
assertTrue(time > DateUtilities.now() - 1000L); assertTrue(time > DateUtilities.now() - 1000L);
assertTrue(time < DateUtilities.now() + 5000L); assertTrue(time < DateUtilities.now() + 5000L);
assertEquals(type, ReminderService.TYPE_DUE); assertEquals(type, ReminderService.TYPE_DUE);
@ -252,17 +248,30 @@ public class ReminderServiceTest extends InjectingTestCase {
// --- helper classes // --- helper classes
public class NoAlarmExpected implements AlarmScheduler { public class NoAlarmExpected implements AlarmScheduler {
public void createAlarm(Context context, Task task, long time, int type) { @Override
public void createAlarm(Task task, long time, int type) {
if(time == 0 || time == Long.MAX_VALUE) if(time == 0 || time == Long.MAX_VALUE)
return; return;
fail("created alarm, no alarm expected (" + type + ": " + newDateTime(time)); fail("created alarm, no alarm expected (" + type + ": " + newDateTime(time));
} }
@Override
public void clear() {
}
} }
public class AlarmExpected implements AlarmScheduler { public class AlarmExpected implements AlarmScheduler {
public boolean alarmCreated = false; public boolean alarmCreated = false;
public void createAlarm(Context context, Task task, long time, int type) {
@Override
public void createAlarm(Task task, long time, int type) {
alarmCreated = true; alarmCreated = true;
} }
@Override
public void clear() {
}
} }
} }

@ -3,7 +3,7 @@
* *
* See the file "LICENSE" for the full license governing this code. * See the file "LICENSE" for the full license governing this code.
*/ */
package org.tasks.scheduling; package org.tasks.jobs;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
@ -90,15 +90,15 @@ public class BackupServiceTests extends DatabaseTestCase {
preferences.setLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0); preferences.setLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0);
// create a backup // create a backup
BackupIntentService service = new BackupIntentService(); BackupJob service = new BackupJob(getTargetContext(), new JobManager(getTargetContext()), xmlExporter, preferences);
service.testBackup(xmlExporter, preferences, getTargetContext()); service.startBackup(getTargetContext());
AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME); AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME);
// assert file created // assert file created
File[] files = temporaryDirectory.listFiles(); File[] files = temporaryDirectory.listFiles();
assertEquals(1, files.length); assertEquals(1, files.length);
assertTrue(files[0].getName().matches(BackupIntentService.BACKUP_FILE_NAME_REGEX)); assertTrue(files[0].getName().matches(BackupJob.BACKUP_FILE_NAME_REGEX));
// assert summary updated // assert summary updated
assertTrue(preferences.getLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0) > 0); assertTrue(preferences.getLong(TasksXmlExporter.PREF_BACKUP_LAST_DATE, 0) > 0);
@ -128,8 +128,8 @@ public class BackupServiceTests extends DatabaseTestCase {
assertEquals(11, files.length); assertEquals(11, files.length);
// backup // backup
BackupIntentService service = new BackupIntentService(); BackupJob service = new BackupJob(getTargetContext(), new JobManager(getTargetContext()), xmlExporter, preferences);
service.testBackup(xmlExporter, preferences, getTargetContext()); service.startBackup(getTargetContext());
AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME); AndroidUtilities.sleepDeep(BACKUP_WAIT_TIME);
@ -138,7 +138,7 @@ public class BackupServiceTests extends DatabaseTestCase {
assertFalse(files[4].exists()); assertFalse(files[4].exists());
// assert user file still exists // assert user file still exists
service.testBackup(xmlExporter, preferences, getTargetContext()); service.startBackup(getTargetContext());
assertTrue(myFile.exists()); assertTrue(myFile.exists());
} }
} }

@ -0,0 +1,134 @@
package org.tasks.jobs;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.tasks.Freeze;
import org.tasks.Snippet;
import org.tasks.preferences.Preferences;
import java.util.concurrent.TimeUnit;
import static com.todoroo.astrid.reminders.ReminderService.TYPE_DUE;
import static java.util.Collections.singletonList;
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.Matchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
@RunWith(AndroidJUnit4.class)
public class JobQueueTest {
private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
private JobQueue<Reminder> queue;
private Preferences preferences;
@Before
public void before() {
preferences = mock(Preferences.class);
when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg());
queue = new JobQueue<>(preferences);
}
@Test
public void twoJobsAtSameTime() {
queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 1, 0));
assertEquals(2, queue.size());
}
@Test
public void rescheduleForFirstJob() {
assertTrue(queue.add(new Reminder(1, 1, 0)));
}
@Test
public void dontRescheduleForLaterJobs() {
queue.add(new Reminder(1, 1, 0));
assertFalse(queue.add(new Reminder(2, 2, 0)));
}
@Test
public void rescheduleForNewerJob() {
queue.add(new Reminder(1, 2, 0));
assertTrue(queue.add(new Reminder(1, 1, 0)));
}
@Test
public void rescheduleWhenCancelingOnlyJob() {
queue.add(new Reminder(1, 2, 0));
assertTrue(queue.cancel(1));
}
@Test
public void rescheduleWhenCancelingFirstJob() {
queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 2, 0));
assertTrue(queue.cancel(1));
}
@Test
public void dontRescheduleWhenCancelingLaterJob() {
queue.add(new Reminder(1, 1, 0));
queue.add(new Reminder(2, 2, 0));
assertFalse(queue.cancel(2));
}
@Test
public void nextScheduledTimeIsZeroWhenQueueIsEmpty() {
when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L);
assertEquals(0, queue.nextScheduledTime());
}
@Test
public void adjustNextScheduledTimeForQuietHours() {
when(preferences.adjustForQuietHours(anyLong())).thenReturn(1234L);
queue.add(new Reminder(1, 1, 1));
assertEquals(1234, queue.nextScheduledTime());
}
@Test
public void overdueJobsAreReturned() {
long now = currentTimeMillis();
queue.add(new Reminder(1, now, TYPE_DUE));
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
Freeze.freezeAt(now).thawAfter(new Snippet() {{
assertEquals(
singletonList(new Reminder(1, now, TYPE_DUE)),
queue.removeOverdueJobs());
}});
}
@Test
public void overdueJobsAreRemoved() {
long now = currentTimeMillis();
queue.add(new Reminder(1, now, TYPE_DUE));
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
Freeze.freezeAt(now).thawAfter(new Snippet() {{
queue.removeOverdueJobs();
}});
assertEquals(
singletonList(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)),
queue.getJobs());
}
}

@ -12,6 +12,7 @@ import static org.tasks.makers.Maker.make;
public class TaskMaker { public class TaskMaker {
public static Property<Task, Long> ID = 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();
@ -26,6 +27,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();
long id = lookup.valueOf(ID, Task.NO_ID);
if (id != Task.NO_ID) {
task.setId(id);
}
DateTime dueDate = lookup.valueOf(DUE_DATE, (DateTime) null); DateTime dueDate = lookup.valueOf(DUE_DATE, (DateTime) null);
if (dueDate != null) { if (dueDate != null) {
task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate.getMillis())); task.setDueDate(Task.createDueDate(Task.URGENCY_SPECIFIC_DAY, dueDate.getMillis()));

@ -1,4 +1,4 @@
package org.tasks.scheduling; package org.tasks.preferences;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
@ -7,7 +7,6 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.tasks.R; import org.tasks.R;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -16,20 +15,18 @@ import static android.support.test.InstrumentationRegistry.getTargetContext;
import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertEquals;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class AlarmManagerTests { public class PreferenceTests {
@SuppressLint("NewApi") @SuppressLint("NewApi")
private static final int MILLIS_PER_HOUR = (int) TimeUnit.HOURS.toMillis(1); private static final int MILLIS_PER_HOUR = (int) TimeUnit.HOURS.toMillis(1);
private Preferences preferences; private Preferences preferences;
private AlarmManager alarmManager;
@Before @Before
public void setUp() { public void setUp() {
preferences = new Preferences(getTargetContext(), null); preferences = new Preferences(getTargetContext(), null);
preferences.clear(); preferences.clear();
preferences.setBoolean(R.string.p_rmd_enable_quiet, true); preferences.setBoolean(R.string.p_rmd_enable_quiet, true);
alarmManager = new AlarmManager(getTargetContext(), preferences);
} }
@Test @Test
@ -40,7 +37,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 8, 0, 1).getMillis(); long dueDate = new DateTime(2015, 12, 29, 8, 0, 1).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -51,7 +48,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 18, 0, 1).getMillis(); long dueDate = new DateTime(2015, 12, 29, 18, 0, 1).getMillis();
assertEquals(new DateTime(2015, 12, 29, 19, 0).getMillis(), assertEquals(new DateTime(2015, 12, 29, 19, 0).getMillis(),
alarmManager.adjustForQuietHours(dueDate)); preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -62,7 +59,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 22, 0, 1).getMillis(); long dueDate = new DateTime(2015, 12, 29, 22, 0, 1).getMillis();
assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(),
alarmManager.adjustForQuietHours(dueDate)); preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -73,7 +70,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 23, 30).getMillis(); long dueDate = new DateTime(2015, 12, 29, 23, 30).getMillis();
assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(),
alarmManager.adjustForQuietHours(dueDate)); preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -84,7 +81,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 30, 7, 15).getMillis(); long dueDate = new DateTime(2015, 12, 30, 7, 15).getMillis();
assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(), assertEquals(new DateTime(2015, 12, 30, 10, 0).getMillis(),
alarmManager.adjustForQuietHours(dueDate)); preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -94,7 +91,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 18, 0, 0).getMillis(); long dueDate = new DateTime(2015, 12, 29, 18, 0, 0).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -104,7 +101,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 18, 0).getMillis(); long dueDate = new DateTime(2015, 12, 29, 18, 0).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -114,7 +111,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 10, 0).getMillis(); long dueDate = new DateTime(2015, 12, 29, 10, 0).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -124,7 +121,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 11, 30).getMillis(); long dueDate = new DateTime(2015, 12, 29, 11, 30).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -134,7 +131,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 22, 15).getMillis(); long dueDate = new DateTime(2015, 12, 29, 22, 15).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
@Test @Test
@ -144,7 +141,7 @@ public class AlarmManagerTests {
long dueDate = new DateTime(2015, 12, 29, 13, 45).getMillis(); long dueDate = new DateTime(2015, 12, 29, 13, 45).getMillis();
assertEquals(dueDate, alarmManager.adjustForQuietHours(dueDate)); assertEquals(dueDate, preferences.adjustForQuietHours(dueDate));
} }
private void setQuietHoursStart(int hour) { private void setQuietHoursStart(int hour) {

@ -19,7 +19,7 @@ import com.todoroo.astrid.subtasks.SubtasksHelperTest;
import com.todoroo.astrid.subtasks.SubtasksTestCase; import com.todoroo.astrid.subtasks.SubtasksTestCase;
import com.todoroo.astrid.sync.NewSyncTestCase; import com.todoroo.astrid.sync.NewSyncTestCase;
import org.tasks.scheduling.BackupServiceTests; import org.tasks.jobs.BackupServiceTests;
import dagger.Component; import dagger.Component;

@ -12,8 +12,6 @@ import org.tasks.receivers.BootCompletedReceiver;
import org.tasks.receivers.CompleteTaskReceiver; import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.ListNotificationReceiver;
import org.tasks.receivers.MyPackageReplacedReceiver; import org.tasks.receivers.MyPackageReplacedReceiver;
import org.tasks.receivers.RefreshReceiver;
import org.tasks.receivers.TaskNotificationReceiver;
import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.receivers.TeslaUnreadReceiver;
import org.tasks.widget.TasksWidget; import org.tasks.widget.TasksWidget;
@ -37,10 +35,6 @@ public interface BroadcastComponent {
void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); void inject(MyPackageReplacedReceiver myPackageReplacedReceiver);
void inject(RefreshReceiver refreshReceiver);
void inject(TaskNotificationReceiver taskNotificationReceiver);
void inject(CompleteTaskReceiver completeTaskReceiver); void inject(CompleteTaskReceiver completeTaskReceiver);
void inject(ListNotificationReceiver listNotificationReceiver); void inject(ListNotificationReceiver listNotificationReceiver);

@ -13,8 +13,6 @@ import org.tasks.receivers.CompleteTaskReceiver;
import org.tasks.receivers.GoogleTaskPushReceiver; import org.tasks.receivers.GoogleTaskPushReceiver;
import org.tasks.receivers.ListNotificationReceiver; import org.tasks.receivers.ListNotificationReceiver;
import org.tasks.receivers.MyPackageReplacedReceiver; import org.tasks.receivers.MyPackageReplacedReceiver;
import org.tasks.receivers.RefreshReceiver;
import org.tasks.receivers.TaskNotificationReceiver;
import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.receivers.TeslaUnreadReceiver;
import org.tasks.widget.TasksWidget; import org.tasks.widget.TasksWidget;
@ -40,10 +38,6 @@ public interface BroadcastComponent {
void inject(MyPackageReplacedReceiver myPackageReplacedReceiver); void inject(MyPackageReplacedReceiver myPackageReplacedReceiver);
void inject(RefreshReceiver refreshReceiver);
void inject(TaskNotificationReceiver taskNotificationReceiver);
void inject(CompleteTaskReceiver completeTaskReceiver); void inject(CompleteTaskReceiver completeTaskReceiver);
void inject(ListNotificationReceiver listNotificationReceiver); void inject(ListNotificationReceiver listNotificationReceiver);

@ -94,6 +94,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Tasks" android:theme="@style/Tasks"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_config"
android:name=".Tasks" android:name=".Tasks"
android:supportsRtl="true" android:supportsRtl="true"
android:manageSpaceActivity="com.todoroo.astrid.core.OldTaskPreferences"> android:manageSpaceActivity="com.todoroo.astrid.core.OldTaskPreferences">
@ -207,14 +208,10 @@
<!-- ======================================================= Receivers = --> <!-- ======================================================= Receivers = -->
<receiver android:name=".receivers.TaskNotificationReceiver" />
<receiver <receiver
android:name=".receivers.ListNotificationReceiver" android:name=".receivers.ListNotificationReceiver"
android:exported="true" /> android:exported="true" />
<receiver android:name=".receivers.RefreshReceiver" />
<!-- widgets --> <!-- widgets -->
<receiver <receiver
android:name=".widget.TasksWidget" android:name=".widget.TasksWidget"
@ -441,13 +438,10 @@
android:name=".scheduling.GeofenceSchedulingIntentService" android:name=".scheduling.GeofenceSchedulingIntentService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".scheduling.BackupIntentService" android:name=".scheduling.SchedulerIntentService"
android:exported="false" />
<service
android:name=".scheduling.RefreshSchedulerIntentService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".scheduling.ReminderSchedulerIntentService" android:name=".scheduling.NotificationSchedulerIntentService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".scheduling.CalendarNotificationIntentService" android:name=".scheduling.CalendarNotificationIntentService"

@ -5,10 +5,7 @@
*/ */
package com.todoroo.astrid.alarms; package com.todoroo.astrid.alarms;
import android.app.PendingIntent;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import com.todoroo.andlib.data.Callback; import com.todoroo.andlib.data.Callback;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
@ -21,13 +18,13 @@ import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria;
import com.todoroo.astrid.dao.TaskDao.TaskCriteria; import com.todoroo.astrid.dao.TaskDao.TaskCriteria;
import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.Metadata;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.reminders.ReminderService;
import com.todoroo.astrid.service.SynchronizeMetadataCallback; import com.todoroo.astrid.service.SynchronizeMetadataCallback;
import org.tasks.injection.ApplicationScope; import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication; import org.tasks.jobs.Alarm;
import org.tasks.receivers.TaskNotificationReceiver; import org.tasks.jobs.JobManager;
import org.tasks.scheduling.AlarmManager; import org.tasks.jobs.JobQueue;
import org.tasks.preferences.Preferences;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -48,15 +45,16 @@ public class AlarmService {
private static final long NO_ALARM = Long.MAX_VALUE; private static final long NO_ALARM = Long.MAX_VALUE;
private final JobQueue<Alarm> jobs;
private final MetadataDao metadataDao; private final MetadataDao metadataDao;
private final Context context; private final JobManager jobManager;
private final AlarmManager alarmManager;
@Inject @Inject
public AlarmService(MetadataDao metadataDao, @ForApplication Context context, AlarmManager alarmManager) { public AlarmService(MetadataDao metadataDao, JobManager jobManager, Preferences preferences) {
this.metadataDao = metadataDao; this.metadataDao = metadataDao;
this.context = context; this.jobManager = jobManager;
this.alarmManager = alarmManager; jobs = new JobQueue<>(preferences);
} }
public void getAlarms(long taskId, Callback<Metadata> callback) { public void getAlarms(long taskId, Callback<Metadata> callback) {
@ -79,11 +77,7 @@ public class AlarmService {
metadata.add(item); metadata.add(item);
} }
boolean changed = synchronizeMetadata(taskId, metadata, m -> { boolean changed = synchronizeMetadata(taskId, metadata, m -> cancelAlarm(m.getId()));
// Cancel the alarm before the metadata is deleted
PendingIntent pendingIntent = pendingIntentForAlarm(m, taskId);
alarmManager.cancel(pendingIntent);
});
if(changed) { if(changed) {
scheduleAlarms(taskId); scheduleAlarms(taskId);
@ -94,18 +88,24 @@ public class AlarmService {
// --- alarm scheduling // --- alarm scheduling
private void getActiveAlarms(Callback<Metadata> callback) { private void getActiveAlarms(Callback<Metadata> callback) {
metadataDao.query(callback, Query.select(Metadata.ID, Metadata.TASK, AlarmFields.TIME). metadataDao.query(callback, Query.select(Metadata.PROPERTIES).
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
where(Criterion.and(TaskCriteria.isActive(), MetadataCriteria.withKey(AlarmFields.METADATA_KEY)))); where(Criterion.and(TaskCriteria.isActive(),
MetadataCriteria.withKey(AlarmFields.METADATA_KEY))));
} }
private void getActiveAlarmsForTask(long taskId, Callback<Metadata> callback) { private void getActiveAlarmsForTask(long taskId, Callback<Metadata> callback) {
metadataDao.query(callback, Query.select(Metadata.ID, Metadata.TASK, AlarmFields.TIME). metadataDao.query(callback, Query.select(Metadata.PROPERTIES).
join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))). join(Join.inner(Task.TABLE, Metadata.TASK.eq(Task.ID))).
where(Criterion.and(TaskCriteria.isActive(), where(Criterion.and(TaskCriteria.isActive(),
MetadataCriteria.byTaskAndwithKey(taskId, AlarmFields.METADATA_KEY)))); MetadataCriteria.byTaskAndwithKey(taskId, AlarmFields.METADATA_KEY))));
} }
public void clear() {
jobs.clear();
jobManager.cancelAlarms();
}
/** /**
* Schedules all alarms * Schedules all alarms
*/ */
@ -120,33 +120,28 @@ public class AlarmService {
getActiveAlarmsForTask(taskId, this::scheduleAlarm); getActiveAlarmsForTask(taskId, this::scheduleAlarm);
} }
private PendingIntent pendingIntentForAlarm(Metadata alarm, long taskId) {
Intent intent = new Intent(context, TaskNotificationReceiver.class);
intent.setAction("ALARM" + alarm.getId()); //$NON-NLS-1$
intent.putExtra(TaskNotificationReceiver.ID_KEY, taskId);
intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, ReminderService.TYPE_ALARM);
return PendingIntent.getBroadcast(context, (int)alarm.getId(),
intent, PendingIntent.FLAG_UPDATE_CURRENT);
}
/** /**
* Schedules alarms for a single task * Schedules alarms for a single task
*/ */
private void scheduleAlarm(Metadata alarm) { private void scheduleAlarm(Metadata metadata) {
if(alarm == null) { if(metadata == null) {
return; return;
} }
long taskId = alarm.getTask(); Alarm alarm = new Alarm(metadata);
long time = alarm.getTime();
PendingIntent pendingIntent = pendingIntentForAlarm(alarm, taskId);
long time = alarm.getValue(AlarmFields.TIME);
if(time == 0 || time == NO_ALARM) { if(time == 0 || time == NO_ALARM) {
alarmManager.cancel(pendingIntent); cancelAlarm(alarm.getId());
} else if(time > DateUtilities.now()) { } else {
alarmManager.wakeupAdjustingForQuietHours(time, pendingIntent); if (jobs.add(alarm)) {
scheduleNext(true);
}
}
}
private void cancelAlarm(Long alarmId) {
if (jobs.cancel(alarmId)) {
scheduleNext(true);
} }
} }
@ -200,4 +195,18 @@ public class AlarmService {
return dirty[0]; return dirty[0];
} }
public void scheduleNextJob() {
scheduleNext(false);
}
private void scheduleNext(boolean cancelCurrent) {
if (!jobs.isEmpty()) {
jobManager.scheduleAlarm(jobs.nextScheduledTime(), cancelCurrent);
}
}
public List<Alarm> removePastDueAlarms() {
return jobs.removeOverdueJobs();
}
} }

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

@ -0,0 +1,74 @@
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,6 +16,8 @@ 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;
@ -29,7 +31,7 @@ import org.tasks.preferences.PermissionChecker;
import org.tasks.preferences.PermissionRequestor; import org.tasks.preferences.PermissionRequestor;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.scheduling.GeofenceSchedulingIntentService; import org.tasks.scheduling.GeofenceSchedulingIntentService;
import org.tasks.scheduling.ReminderSchedulerIntentService; import org.tasks.scheduling.NotificationSchedulerIntentService;
import org.tasks.themes.LEDColor; import org.tasks.themes.LEDColor;
import org.tasks.themes.ThemeCache; import org.tasks.themes.ThemeCache;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
@ -53,6 +55,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject Preferences preferences; @Inject Preferences preferences;
@Inject ThemeCache themeCache; @Inject ThemeCache themeCache;
@Inject AlarmService alarmService;
private CheckBoxPreference fieldMissedCalls; private CheckBoxPreference fieldMissedCalls;
@ -96,7 +99,7 @@ public class ReminderPreferences extends InjectingPreferenceActivity {
private void rescheduleNotificationsOnChange(int... resIds) { private void rescheduleNotificationsOnChange(int... resIds) {
for (int resId : resIds) { for (int resId : resIds) {
findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> { findPreference(getString(resId)).setOnPreferenceChangeListener((preference, newValue) -> {
startService(new Intent(ReminderPreferences.this, ReminderSchedulerIntentService.class)); startService(new Intent(ReminderPreferences.this, NotificationSchedulerIntentService.class));
return true; return true;
}); });
} }

@ -5,11 +5,6 @@
*/ */
package com.todoroo.astrid.reminders; package com.todoroo.astrid.reminders;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.todoroo.andlib.data.Property; import com.todoroo.andlib.data.Property;
import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Criterion;
import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.sql.Query;
@ -20,18 +15,13 @@ 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.injection.ForApplication;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.receivers.TaskNotificationReceiver;
import org.tasks.scheduling.AlarmManager;
import org.tasks.time.DateTime; import org.tasks.time.DateTime;
import java.util.Random; import java.util.Random;
import javax.inject.Inject; import javax.inject.Inject;
import timber.log.Timber;
import static org.tasks.date.DateTimeUtils.newDateTime; import static org.tasks.date.DateTimeUtils.newDateTime;
/** /**
@ -76,14 +66,12 @@ public final class ReminderService {
private AlarmScheduler scheduler; 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 Context context;
private final Preferences preferences; private final Preferences preferences;
@Inject @Inject
ReminderService(@ForApplication Context context, Preferences preferences, AlarmManager alarmManager) { ReminderService(Preferences preferences, ReminderAlarmScheduler scheduler) {
this.context = context;
this.preferences = preferences; this.preferences = preferences;
scheduler = new ReminderAlarmScheduler(alarmManager); this.scheduler = scheduler;
} }
private static final int MILLIS_PER_HOUR = 60 * 60 * 1000; private static final int MILLIS_PER_HOUR = 60 * 60 * 1000;
@ -102,6 +90,10 @@ public final class ReminderService {
now = -1; // Signal done with now variable now = -1; // Signal done with now variable
} }
public void clear() {
scheduler.clear();
}
private long getNowValue() { private long getNowValue() {
// If we're in the midst of mass scheduling, use the prestored now var // If we're in the midst of mass scheduling, use the prestored now var
return (now == -1 ? DateUtilities.now() : now); return (now == -1 ? DateUtilities.now() : now);
@ -117,10 +109,7 @@ public final class ReminderService {
} }
private void clearAllAlarms(Task task) { private void clearAllAlarms(Task task) {
scheduler.createAlarm(context, task, NO_ALARM, TYPE_SNOOZE); scheduler.createAlarm(task, NO_ALARM, 0);
scheduler.createAlarm(context, task, NO_ALARM, TYPE_RANDOM);
scheduler.createAlarm(context, task, NO_ALARM, TYPE_DUE);
scheduler.createAlarm(context, task, NO_ALARM, TYPE_OVERDUE);
} }
private void scheduleAlarm(Task task, TaskDao taskDao) { private void scheduleAlarm(Task task, TaskDao taskDao) {
@ -175,15 +164,15 @@ public final class ReminderService {
// snooze trumps all // snooze trumps all
if(whenSnooze != NO_ALARM) { if(whenSnooze != NO_ALARM) {
scheduler.createAlarm(context, task, whenSnooze, TYPE_SNOOZE); scheduler.createAlarm(task, whenSnooze, TYPE_SNOOZE);
} else if(whenRandom < whenDueDate && whenRandom < whenOverdue) { } else if(whenRandom < whenDueDate && whenRandom < whenOverdue) {
scheduler.createAlarm(context, task, whenRandom, TYPE_RANDOM); scheduler.createAlarm(task, whenRandom, TYPE_RANDOM);
} else if(whenDueDate < whenOverdue) { } else if(whenDueDate < whenOverdue) {
scheduler.createAlarm(context, task, whenDueDate, TYPE_DUE); scheduler.createAlarm(task, whenDueDate, TYPE_DUE);
} else if(whenOverdue != NO_ALARM) { } else if(whenOverdue != NO_ALARM) {
scheduler.createAlarm(context, task, whenOverdue, TYPE_OVERDUE); scheduler.createAlarm(task, whenOverdue, TYPE_OVERDUE);
} else { } else {
scheduler.createAlarm(context, task, 0, 0); scheduler.createAlarm(task, 0, 0);
} }
} }
@ -299,13 +288,6 @@ public final class ReminderService {
// --- alarm manager alarm creation // --- alarm manager alarm creation
/**
* Interface for testing
*/
public interface AlarmScheduler {
void createAlarm(Context context, Task task, long time, int type);
}
public void setScheduler(AlarmScheduler scheduler) { public void setScheduler(AlarmScheduler scheduler) {
this.scheduler = scheduler; this.scheduler = scheduler;
} }
@ -313,50 +295,4 @@ public final class ReminderService {
public AlarmScheduler getScheduler() { public AlarmScheduler getScheduler() {
return scheduler; return scheduler;
} }
private static class ReminderAlarmScheduler implements AlarmScheduler {
private final AlarmManager alarmManager;
public ReminderAlarmScheduler(AlarmManager alarmManager) {
this.alarmManager = alarmManager;
}
/**
* Create an alarm for the given task at the given type
*/
@Override
public void createAlarm(Context context, Task task, long time, int type) {
if(task.getId() == Task.NO_ID) {
return;
}
Intent intent = new Intent(context, TaskNotificationReceiver.class);
intent.setType(Long.toString(task.getId()));
intent.setAction(Integer.toString(type));
intent.putExtra(TaskNotificationReceiver.ID_KEY, task.getId());
intent.putExtra(TaskNotificationReceiver.EXTRAS_TYPE, type);
// calculate the unique requestCode as a combination of the task-id and alarm-type:
// concatenate id+type to keep the combo unique
@SuppressLint("DefaultLocale") String rc = String.format("%d%d", task.getId(), type);
int requestCode;
try {
requestCode = Integer.parseInt(rc);
} catch (Exception e) {
Timber.e(e, e.getMessage());
requestCode = type;
}
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, requestCode,
intent, 0);
if (time == 0 || time == NO_ALARM) {
alarmManager.cancel(pendingIntent);
} else {
if(time < DateUtilities.now()) {
time = DateUtilities.now() + 5000L;
}
alarmManager.wakeupAdjustingForQuietHours(time, pendingIntent);
}
}
}
} }

@ -6,6 +6,8 @@ import com.todoroo.astrid.service.StartupService;
import org.tasks.analytics.Tracker; import org.tasks.analytics.Tracker;
import org.tasks.injection.ApplicationComponent; import org.tasks.injection.ApplicationComponent;
import org.tasks.injection.InjectingApplication; import org.tasks.injection.InjectingApplication;
import org.tasks.jobs.JobManager;
import org.tasks.jobs.JobCreator;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import org.tasks.receivers.TeslaUnreadReceiver; import org.tasks.receivers.TeslaUnreadReceiver;
import org.tasks.themes.ThemeCache; import org.tasks.themes.ThemeCache;
@ -21,6 +23,8 @@ public class Tasks extends InjectingApplication {
@Inject BuildSetup buildSetup; @Inject BuildSetup buildSetup;
@Inject ThemeCache themeCache; @Inject ThemeCache themeCache;
@Inject TeslaUnreadReceiver teslaUnreadReceiver; @Inject TeslaUnreadReceiver teslaUnreadReceiver;
@Inject JobManager jobManager;
@Inject JobCreator jobCreator;
@Override @Override
public void onCreate() { public void onCreate() {
@ -28,11 +32,14 @@ public class Tasks extends InjectingApplication {
tracker.setTrackingEnabled(preferences.isTrackingEnabled()); tracker.setTrackingEnabled(preferences.isTrackingEnabled());
AndroidThreeTen.init(this);
if (!buildSetup.setup()) { if (!buildSetup.setup()) {
return; return;
} }
AndroidThreeTen.init(this);
jobManager.addJobCreator(jobCreator);
flavorSetup.setup(); flavorSetup.setup();
teslaUnreadReceiver.setEnabled(preferences.getBoolean(R.string.p_tesla_unread_enabled, false)); teslaUnreadReceiver.setEnabled(preferences.getBoolean(R.string.p_tesla_unread_enabled, false));

@ -1,25 +1,22 @@
package org.tasks.injection; package org.tasks.injection;
import org.tasks.location.GeofenceTransitionsIntentService; import org.tasks.location.GeofenceTransitionsIntentService;
import org.tasks.scheduling.BackupIntentService;
import org.tasks.scheduling.CalendarNotificationIntentService; import org.tasks.scheduling.CalendarNotificationIntentService;
import org.tasks.scheduling.GeofenceSchedulingIntentService; import org.tasks.scheduling.GeofenceSchedulingIntentService;
import org.tasks.scheduling.RefreshSchedulerIntentService; import org.tasks.scheduling.NotificationSchedulerIntentService;
import org.tasks.scheduling.ReminderSchedulerIntentService; import org.tasks.scheduling.SchedulerIntentService;
import dagger.Subcomponent; import dagger.Subcomponent;
@Subcomponent(modules = IntentServiceModule.class) @Subcomponent(modules = IntentServiceModule.class)
public interface IntentServiceComponent { public interface IntentServiceComponent {
void inject(ReminderSchedulerIntentService reminderSchedulerIntentService); void inject(SchedulerIntentService schedulerIntentService);
void inject(RefreshSchedulerIntentService refreshSchedulerIntentService);
void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService); void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService);
void inject(CalendarNotificationIntentService calendarNotificationIntentService); void inject(CalendarNotificationIntentService calendarNotificationIntentService);
void inject(BackupIntentService backupIntentService);
void inject(GeofenceTransitionsIntentService geofenceTransitionsIntentService); void inject(GeofenceTransitionsIntentService geofenceTransitionsIntentService);
void inject(NotificationSchedulerIntentService notificationSchedulerIntentService);
} }

@ -0,0 +1,58 @@
package org.tasks.jobs;
import com.todoroo.astrid.alarms.AlarmFields;
import com.todoroo.astrid.data.Metadata;
public class Alarm implements JobQueueEntry {
private final long alarmId;
private final long taskId;
private final long time;
public Alarm(Metadata metadata) {
this(metadata.getId(), metadata.getTask(), metadata.getValue(AlarmFields.TIME));
}
public Alarm(long alarmId, long taskId, Long time) {
this.alarmId = alarmId;
this.taskId = taskId;
this.time = time;
}
@Override
public long getId() {
return alarmId;
}
public long getTaskId() {
return taskId;
}
@Override
public long getTime() {
return time;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Alarm alarm = (Alarm) o;
return alarmId == alarm.alarmId;
}
@Override
public int hashCode() {
return (int) (alarmId ^ (alarmId >>> 32));
}
@Override
public String toString() {
return "Alarm{" +
"alarmId=" + alarmId +
", taskId=" + taskId +
", time=" + time +
'}';
}
}

@ -0,0 +1,47 @@
package org.tasks.jobs;
import android.support.annotation.NonNull;
import com.evernote.android.job.Job;
import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import com.todoroo.astrid.reminders.ReminderService;
import org.tasks.Notifier;
import org.tasks.preferences.Preferences;
public class AlarmJob extends Job {
public static final String TAG = "job_alarm";
private final Preferences preferences;
private final AlarmService alarmService;
private final Notifier notifier;
private final TaskDao taskDao;
public AlarmJob(Preferences preferences, AlarmService alarmService, Notifier notifier, TaskDao taskDao) {
this.preferences = preferences;
this.alarmService = alarmService;
this.notifier = notifier;
this.taskDao = taskDao;
}
@NonNull
@Override
protected Result onRunJob(Params params) {
try {
if (!preferences.isCurrentlyQuietHours()) {
for (Alarm alarm : alarmService.removePastDueAlarms()) {
Task task = taskDao.fetch(alarm.getTaskId(), Task.REMINDER_LAST);
if (task != null && task.getReminderLast() < alarm.getTime()) {
notifier.triggerTaskNotification(alarm.getTaskId(), ReminderService.TYPE_ALARM);
}
}
}
return Result.SUCCESS;
} finally {
alarmService.scheduleNextJob();
}
}
}

@ -1,55 +1,50 @@
package org.tasks.scheduling; package org.tasks.jobs;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull;
import com.evernote.android.job.Job;
import com.todoroo.astrid.backup.TasksXmlExporter; import com.todoroo.astrid.backup.TasksXmlExporter;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.preferences.Preferences; import org.tasks.preferences.Preferences;
import java.io.File; import java.io.File;
import java.io.FileFilter; import java.io.FileFilter;
import java.util.Arrays; import java.util.Arrays;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
public class BackupIntentService extends MidnightIntentService { public class BackupJob extends Job {
public static final String TAG = "job_backup";
public static final String BACKUP_FILE_NAME_REGEX = "auto\\.[-\\d]+\\.xml"; //$NON-NLS-1$ public static final String BACKUP_FILE_NAME_REGEX = "auto\\.[-\\d]+\\.xml"; //$NON-NLS-1$
private static final int DAYS_TO_KEEP_BACKUP = 7; private static final int DAYS_TO_KEEP_BACKUP = 7;
@Inject TasksXmlExporter xmlExporter; private final Context context;
@Inject Preferences preferences; private final JobManager jobManager;
private TasksXmlExporter tasksXmlExporter;
public BackupIntentService() { private Preferences preferences;
super(BackupIntentService.class.getSimpleName());
}
@Override public BackupJob(Context context, JobManager jobManager, TasksXmlExporter tasksXmlExporter, Preferences preferences) {
void run() { this.context = context;
startBackup(this); this.jobManager = jobManager;
this.tasksXmlExporter = tasksXmlExporter;
this.preferences = preferences;
} }
@NonNull
@Override @Override
protected String getLastRunPreference() { protected Result onRunJob(Params params) {
return TasksXmlExporter.PREF_BACKUP_LAST_DATE; try {
}
/**
* Test hook for backup
*/
void testBackup(TasksXmlExporter xmlExporter, Preferences preferences, Context context) {
this.xmlExporter = xmlExporter;
this.preferences = preferences;
startBackup(context); startBackup(context);
return Result.SUCCESS;
} finally {
jobManager.scheduleMidnightBackup(false);
} }
private void startBackup(Context context) {
if (context == null || context.getResources() == null) {
return;
} }
void startBackup(Context context) {
try { try {
deleteOldBackups(); deleteOldBackups();
} catch (Exception e) { } catch (Exception e) {
@ -57,7 +52,7 @@ public class BackupIntentService extends MidnightIntentService {
} }
try { try {
xmlExporter.exportTasks(context, TasksXmlExporter.ExportType.EXPORT_TYPE_SERVICE, null); tasksXmlExporter.exportTasks(context, TasksXmlExporter.ExportType.EXPORT_TYPE_SERVICE, null);
} catch (Exception e) { } catch (Exception e) {
Timber.e(e, e.getMessage()); Timber.e(e, e.getMessage());
} }
@ -88,9 +83,4 @@ public class BackupIntentService extends MidnightIntentService {
} }
} }
} }
@Override
protected void inject(IntentServiceComponent component) {
component.inject(this);
}
} }

@ -0,0 +1,68 @@
package org.tasks.jobs;
import android.content.Context;
import com.evernote.android.job.Job;
import com.todoroo.astrid.alarms.AlarmService;
import com.todoroo.astrid.backup.TasksXmlExporter;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.reminders.ReminderAlarmScheduler;
import org.tasks.Broadcaster;
import org.tasks.Notifier;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import org.tasks.scheduling.RefreshScheduler;
import javax.inject.Inject;
@ApplicationScope
public class JobCreator implements com.evernote.android.job.JobCreator {
private final Context context;
private final Notifier notifier;
private final JobManager jobManager;
private final Broadcaster broadcaster;
private final TasksXmlExporter tasksXmlExporter;
private final Preferences preferences;
private final RefreshScheduler refreshScheduler;
private final AlarmService alarmService;
private final TaskDao taskDao;
private final ReminderAlarmScheduler reminderAlarmScheduler;
@Inject
public JobCreator(@ForApplication Context context, Notifier notifier, JobManager jobManager,
Broadcaster broadcaster, TasksXmlExporter tasksXmlExporter,
Preferences preferences, RefreshScheduler refreshScheduler,
AlarmService alarmService, TaskDao taskDao, ReminderAlarmScheduler reminderAlarmScheduler) {
this.context = context;
this.notifier = notifier;
this.jobManager = jobManager;
this.broadcaster = broadcaster;
this.tasksXmlExporter = tasksXmlExporter;
this.preferences = preferences;
this.refreshScheduler = refreshScheduler;
this.alarmService = alarmService;
this.taskDao = taskDao;
this.reminderAlarmScheduler = reminderAlarmScheduler;
}
@Override
public Job create(String tag) {
switch (tag) {
case ReminderJob.TAG:
return new ReminderJob(preferences, reminderAlarmScheduler, notifier);
case AlarmJob.TAG:
return new AlarmJob(preferences, alarmService, notifier, taskDao);
case RefreshJob.TAG:
return new RefreshJob(refreshScheduler, broadcaster);
case MidnightRefreshJob.TAG:
return new MidnightRefreshJob(broadcaster, jobManager);
case BackupJob.TAG:
return new BackupJob(context, jobManager, tasksXmlExporter, preferences);
default:
return null;
}
}
}

@ -0,0 +1,83 @@
package org.tasks.jobs;
import android.content.Context;
import com.evernote.android.job.JobCreator;
import com.evernote.android.job.JobRequest;
import org.tasks.injection.ApplicationScope;
import org.tasks.injection.ForApplication;
import javax.inject.Inject;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.nextMidnight;
@ApplicationScope
public class JobManager {
private final com.evernote.android.job.JobManager jobManager;
@Inject
public JobManager(@ForApplication Context context) {
jobManager = com.evernote.android.job.JobManager.create(context);
jobManager.cancelAll();
}
public void addJobCreator(JobCreator jobCreator) {
jobManager.addJobCreator(jobCreator);
}
public void scheduleAlarm(long time, boolean cancelCurrent) {
new JobRequest.Builder(AlarmJob.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))
.setUpdateCurrent(cancelCurrent)
.build()
.schedule();
}
public void scheduleRefresh(long time, boolean cancelExisting) {
new JobRequest.Builder(RefreshJob.TAG)
.setExact(Math.max(time - currentTimeMillis(), 5000))
.setUpdateCurrent(cancelExisting)
.build()
.schedule();
}
public void scheduleMidnightRefresh(boolean cancelExisting) {
scheduleMidnightJob(MidnightRefreshJob.TAG, cancelExisting);
}
public void scheduleMidnightBackup(boolean cancelExisting) {
scheduleMidnightJob(BackupJob.TAG, cancelExisting);
}
private void scheduleMidnightJob(String tag, boolean cancelExisting) {
long now = System.currentTimeMillis();
new JobRequest.Builder(tag)
.setExact(nextMidnight(now) - now)
.setUpdateCurrent(cancelExisting)
.build()
.schedule();
}
public void cancelAlarms() {
jobManager.cancelAllForTag(AlarmJob.TAG);
}
public void cancelRefreshes() {
jobManager.cancelAllForTag(RefreshJob.TAG);
}
public void cancelReminders() {
jobManager.cancelAllForTag(ReminderJob.TAG);
}
}

@ -0,0 +1,75 @@
package org.tasks.jobs;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.common.collect.TreeMultimap;
import com.google.common.primitives.Longs;
import org.tasks.preferences.Preferences;
import java.util.List;
import java.util.SortedSet;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Lists.newArrayList;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
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 Preferences preferences;
public JobQueue(Preferences preferences) {
this.preferences = preferences;
}
public boolean add(T entry) {
boolean result = jobs.isEmpty() || entry.getTime() < firstTime();
jobs.put(entry.getTime(), entry);
return result;
}
public boolean isEmpty() {
return jobs.isEmpty();
}
public void clear() {
jobs.clear();
}
public boolean cancel(long id) {
boolean reschedule = false;
long firstTime = firstTime();
List<T> existing = newArrayList(filter(jobs.values(), r -> r.getId() == id));
for (T entry : existing) {
reschedule |= entry.getTime() == firstTime;
jobs.remove(entry.getTime(), entry);
}
return reschedule;
}
List<T> getJobs() {
return ImmutableList.copyOf(jobs.values());
}
public List<T> removeOverdueJobs() {
List<T> result = newArrayList();
SortedSet<Long> lapsed = jobs.keySet().headSet(currentTimeMillis() + 1);
for (Long key : lapsed) {
result.addAll(jobs.removeAll(key));
}
return result;
}
public int size() {
return jobs.size();
}
private long firstTime() {
return jobs.isEmpty() ? 0 : jobs.asMap().firstKey();
}
public long nextScheduledTime() {
long next = firstTime();
return next > 0 ? preferences.adjustForQuietHours(next) : 0;
}
}

@ -0,0 +1,7 @@
package org.tasks.jobs;
public interface JobQueueEntry {
long getId();
long getTime();
}

@ -0,0 +1,31 @@
package org.tasks.jobs;
import android.support.annotation.NonNull;
import com.evernote.android.job.Job;
import org.tasks.Broadcaster;
public class MidnightRefreshJob extends Job {
public static final String TAG = "job_midnight_refresh";
private final Broadcaster broadcaster;
private final JobManager jobManager;
public MidnightRefreshJob(Broadcaster broadcaster, JobManager jobManager) {
this.broadcaster = broadcaster;
this.jobManager = jobManager;
}
@NonNull
@Override
protected Result onRunJob(Params params) {
try {
broadcaster.refresh();
return Result.SUCCESS;
} finally {
jobManager.scheduleMidnightRefresh(false);
}
}
}

@ -0,0 +1,32 @@
package org.tasks.jobs;
import android.support.annotation.NonNull;
import com.evernote.android.job.Job;
import org.tasks.Broadcaster;
import org.tasks.scheduling.RefreshScheduler;
public class RefreshJob extends Job {
public static final String TAG = "job_refresh";
private final RefreshScheduler refreshScheduler;
private final Broadcaster broadcaster;
public RefreshJob(RefreshScheduler refreshScheduler, Broadcaster broadcaster) {
this.refreshScheduler = refreshScheduler;
this.broadcaster = broadcaster;
}
@NonNull
@Override
protected Result onRunJob(Params params) {
try {
broadcaster.refresh();
return Result.SUCCESS;
} finally {
refreshScheduler.scheduleNext();
}
}
}

@ -0,0 +1,57 @@
package org.tasks.jobs;
public class Reminder implements JobQueueEntry {
private final long taskId;
private final long time;
private final int type;
public Reminder(long taskId, long time, int type) {
this.taskId = taskId;
this.time = time;
this.type = type;
}
@Override
public long getId() {
return taskId;
}
@Override
public long getTime() {
return time;
}
public int getType() {
return type;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Reminder reminder = (Reminder) o;
if (taskId != reminder.taskId) return false;
if (time != reminder.time) return false;
return type == reminder.type;
}
@Override
public int hashCode() {
int result = (int) (taskId ^ (taskId >>> 32));
result = 31 * result + (int) (time ^ (time >>> 32));
result = 31 * result + type;
return result;
}
@Override
public String toString() {
return "Reminder{" +
"taskId=" + taskId +
", time=" + time +
", type=" + type +
'}';
}
}

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

@ -56,6 +56,40 @@ public class Preferences {
return getBoolean(R.string.p_back_button_saves_task, false); return getBoolean(R.string.p_back_button_saves_task, false);
} }
public boolean isCurrentlyQuietHours() {
if (quietHoursEnabled()) {
DateTime dateTime = new DateTime();
DateTime start = dateTime.withMillisOfDay(getQuietHoursStart());
DateTime end = dateTime.withMillisOfDay(getQuietHoursEnd());
if (start.isAfter(end)) {
return dateTime.isBefore(end) || dateTime.isAfter(start);
} else {
return dateTime.isAfter(start) && dateTime.isBefore(end);
}
}
return false;
}
public long adjustForQuietHours(long time) {
if (quietHoursEnabled()) {
DateTime dateTime = new DateTime(time);
DateTime start = dateTime.withMillisOfDay(getQuietHoursStart());
DateTime end = dateTime.withMillisOfDay(getQuietHoursEnd());
if (start.isAfter(end)) {
if (dateTime.isBefore(end)) {
return end.getMillis();
} else if (dateTime.isAfter(start)) {
return end.plusDays(1).getMillis();
}
} else {
if (dateTime.isAfter(start) && dateTime.isBefore(end)) {
return end.getMillis();
}
}
}
return time;
}
public boolean quietHoursEnabled() { public boolean quietHoursEnabled() {
return getBoolean(R.string.p_rmd_enable_quiet, false); return getBoolean(R.string.p_rmd_enable_quiet, false);
} }

@ -1,31 +0,0 @@
package org.tasks.receivers;
import android.content.Context;
import android.content.Intent;
import org.tasks.Broadcaster;
import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.InjectingBroadcastReceiver;
import javax.inject.Inject;
import timber.log.Timber;
public class RefreshReceiver extends InjectingBroadcastReceiver {
@Inject Broadcaster broadcaster;
@Override
public void onReceive(Context context, Intent intent) {
super.onReceive(context, intent);
Timber.d("onReceive(context, %s)", intent);
broadcaster.refresh();
}
@Override
protected void inject(BroadcastComponent component) {
component.inject(this);
}
}

@ -1,37 +0,0 @@
package org.tasks.receivers;
import android.content.Context;
import android.content.Intent;
import org.tasks.Notifier;
import org.tasks.injection.BroadcastComponent;
import org.tasks.injection.InjectingBroadcastReceiver;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Inject;
public class TaskNotificationReceiver extends InjectingBroadcastReceiver {
private static final ExecutorService executorService = Executors.newSingleThreadExecutor();
public static final String ID_KEY = "id"; //$NON-NLS-1$
public static final String EXTRAS_TYPE = "type"; //$NON-NLS-1$
@Inject Notifier notifier;
@Override
public void onReceive(Context context, final Intent intent) {
super.onReceive(context, intent);
executorService.execute(() -> notifier.triggerTaskNotification(
intent.getLongExtra(ID_KEY, 0),
intent.getIntExtra(EXTRAS_TYPE, (byte) 0)));
}
@Override
protected void inject(BroadcastComponent component) {
component.inject(this);
}
}

@ -5,8 +5,6 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import org.tasks.injection.ForApplication; import org.tasks.injection.ForApplication;
import org.tasks.preferences.Preferences;
import org.tasks.time.DateTime;
import javax.inject.Inject; import javax.inject.Inject;
@ -16,11 +14,9 @@ import static com.todoroo.andlib.utility.AndroidUtilities.atLeastMarshmallow;
public class AlarmManager { public class AlarmManager {
private final android.app.AlarmManager alarmManager; private final android.app.AlarmManager alarmManager;
private final Preferences preferences;
@Inject @Inject
public AlarmManager(@ForApplication Context context, Preferences preferences) { public AlarmManager(@ForApplication Context context) {
this.preferences = preferences;
alarmManager = (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmManager = (android.app.AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
} }
@ -28,10 +24,6 @@ public class AlarmManager {
alarmManager.cancel(pendingIntent); alarmManager.cancel(pendingIntent);
} }
public void wakeupAdjustingForQuietHours(long time, PendingIntent pendingIntent) {
wakeup(adjustForQuietHours(time), pendingIntent);
}
@SuppressLint("NewApi") @SuppressLint("NewApi")
public void wakeup(long time, PendingIntent pendingIntent) { public void wakeup(long time, PendingIntent pendingIntent) {
if (atLeastMarshmallow()) { if (atLeastMarshmallow()) {
@ -42,35 +34,4 @@ public class AlarmManager {
alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, time, pendingIntent); alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, time, pendingIntent);
} }
} }
@SuppressLint("NewApi")
public void noWakeup(long time, PendingIntent pendingIntent) {
if (atLeastMarshmallow()) {
alarmManager.setExactAndAllowWhileIdle(android.app.AlarmManager.RTC, time, pendingIntent);
} else if (atLeastKitKat()) {
alarmManager.setExact(android.app.AlarmManager.RTC, time, pendingIntent);
} else {
alarmManager.set(android.app.AlarmManager.RTC, time, pendingIntent);
}
}
long adjustForQuietHours(long time) {
if (preferences.quietHoursEnabled()) {
DateTime dateTime = new DateTime(time);
DateTime start = dateTime.withMillisOfDay(preferences.getQuietHoursStart());
DateTime end = dateTime.withMillisOfDay(preferences.getQuietHoursEnd());
if (start.isAfter(end)) {
if (dateTime.isBefore(end)) {
return end.getMillis();
} else if (dateTime.isAfter(start)) {
return end.plusDays(1).getMillis();
}
} else {
if (dateTime.isAfter(start) && dateTime.isBefore(end)) {
return end.getMillis();
}
}
}
return time;
}
} }

@ -17,20 +17,11 @@ public class BackgroundScheduler {
public void scheduleEverything() { public void scheduleEverything() {
context.startService(new Intent(context, GeofenceSchedulingIntentService.class)); context.startService(new Intent(context, GeofenceSchedulingIntentService.class));
context.startService(new Intent(context, ReminderSchedulerIntentService.class)); context.startService(new Intent(context, SchedulerIntentService.class));
scheduleBackupService(); context.startService(new Intent(context, NotificationSchedulerIntentService.class));
scheduleMidnightRefresh();
scheduleCalendarNotifications(); scheduleCalendarNotifications();
} }
public void scheduleBackupService() {
context.startService(new Intent(context, BackupIntentService.class));
}
public void scheduleMidnightRefresh() {
context.startService(new Intent(context, RefreshSchedulerIntentService.class));
}
public void scheduleCalendarNotifications() { public void scheduleCalendarNotifications() {
context.startService(new Intent(context, CalendarNotificationIntentService.class)); context.startService(new Intent(context, CalendarNotificationIntentService.class));
} }

@ -1,62 +0,0 @@
package org.tasks.scheduling;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import org.tasks.injection.InjectingIntentService;
import org.tasks.preferences.Preferences;
import timber.log.Timber;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.nextMidnight;
import static org.tasks.time.DateTimeUtils.printTimestamp;
public abstract class MidnightIntentService extends InjectingIntentService {
private static final long PADDING = SECONDS.toMillis(1);
private final String name;
MidnightIntentService(String name) {
super(name);
this.name = name;
}
@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
Context context = getApplicationContext();
Preferences preferences = new Preferences(context);
AlarmManager alarmManager = new AlarmManager(context, preferences);
String lastRunPreference = getLastRunPreference();
long lastRun = lastRunPreference == null ? 0 : preferences.getLong(lastRunPreference, 0);
long nextRun = nextMidnight(lastRun);
long now = currentTimeMillis();
if (nextRun <= now) {
nextRun = nextMidnight(now);
Timber.d("%s running now [nextRun=%s]", name, printTimestamp(nextRun));
if (!isNullOrEmpty(getLastRunPreference())) {
preferences.setLong(getLastRunPreference(), now);
}
run();
} else {
Timber.d("%s will run at %s [lastRun=%s]", name, printTimestamp(nextRun), printTimestamp(lastRun));
}
PendingIntent pendingIntent = PendingIntent.getService(this, 0, new Intent(this, this.getClass()), PendingIntent.FLAG_UPDATE_CURRENT);
alarmManager.noWakeup(nextRun + PADDING, pendingIntent);
}
abstract void run();
String getLastRunPreference() {
return null;
}
}

@ -13,14 +13,14 @@ import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
public class ReminderSchedulerIntentService extends InjectingIntentService { public class NotificationSchedulerIntentService extends InjectingIntentService {
@Inject AlarmService alarmService; @Inject AlarmService alarmService;
@Inject ReminderService reminderService; @Inject ReminderService reminderService;
@Inject TaskDao taskDao; @Inject TaskDao taskDao;
public ReminderSchedulerIntentService() { public NotificationSchedulerIntentService() {
super(ReminderSchedulerIntentService.class.getSimpleName()); super(NotificationSchedulerIntentService.class.getSimpleName());
} }
@Override @Override
@ -29,6 +29,9 @@ public class ReminderSchedulerIntentService extends InjectingIntentService {
Timber.d("onHandleIntent(%s)", intent); Timber.d("onHandleIntent(%s)", intent);
reminderService.clear();
alarmService.clear();
reminderService.scheduleAllAlarms(taskDao); reminderService.scheduleAllAlarms(taskDao);
alarmService.scheduleAllAlarms(); alarmService.scheduleAllAlarms();
} }

@ -1,33 +1,33 @@
package org.tasks.scheduling; package org.tasks.scheduling;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.todoroo.astrid.data.Task; import com.todoroo.astrid.data.Task;
import org.tasks.injection.ForApplication; import org.tasks.injection.ApplicationScope;
import org.tasks.receivers.RefreshReceiver; import org.tasks.jobs.JobManager;
import javax.inject.Inject; import java.util.SortedSet;
import java.util.TreeSet;
import timber.log.Timber; import javax.inject.Inject;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; import static com.google.common.collect.Lists.newArrayList;
import static com.todoroo.andlib.utility.DateUtilities.ONE_MINUTE; import static com.todoroo.andlib.utility.DateUtilities.ONE_MINUTE;
import static org.tasks.time.DateTimeUtils.currentTimeMillis; import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.nextMidnight;
import static org.tasks.time.DateTimeUtils.printTimestamp;
@ApplicationScope
public class RefreshScheduler { public class RefreshScheduler {
private final Context context; private final JobManager jobManager;
private final AlarmManager alarmManager; private final SortedSet<Long> jobs = new TreeSet<>();
@Inject @Inject
public RefreshScheduler(@ForApplication Context context, AlarmManager alarmManager) { public RefreshScheduler(JobManager jobManager) {
this.context = context; this.jobManager = jobManager;
this.alarmManager = alarmManager; }
public void clear() {
jobs.clear();
jobManager.cancelRefreshes();
} }
public void scheduleRefresh(Task task) { public void scheduleRefresh(Task task) {
@ -43,13 +43,30 @@ public class RefreshScheduler {
private void scheduleRefresh(Long refreshTime) { private void scheduleRefresh(Long refreshTime) {
long now = currentTimeMillis(); long now = currentTimeMillis();
if (now < refreshTime && refreshTime < nextMidnight(now)) { if (now < refreshTime) {
refreshTime += 1000; // this is ghetto refreshTime += 1000; // this is ghetto
Timber.d("Scheduling refresh at %s", printTimestamp(refreshTime)); schedule(refreshTime);
Intent intent = new Intent(context, RefreshReceiver.class); }
intent.setAction(Long.toString(refreshTime)); }
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT);
alarmManager.noWakeup(refreshTime, pendingIntent); public void scheduleNext() {
scheduleNext(false);
}
private void schedule(long timestamp) {
SortedSet<Long> upcoming = jobs.tailSet(currentTimeMillis());
boolean reschedule = upcoming.isEmpty() || timestamp < upcoming.first();
jobs.add(timestamp);
if (reschedule) {
scheduleNext(true);
}
}
private void scheduleNext(boolean cancelCurrent) {
long now = currentTimeMillis();
jobs.removeAll(newArrayList(jobs.headSet(now + 1)));
if (!jobs.isEmpty()) {
jobManager.scheduleRefresh(jobs.first(), cancelCurrent);
} }
} }
} }

@ -1,44 +0,0 @@
package org.tasks.scheduling;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import org.tasks.Broadcaster;
import org.tasks.injection.IntentServiceComponent;
import javax.inject.Inject;
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
import static org.tasks.time.DateTimeUtils.nextMidnight;
public class RefreshSchedulerIntentService extends MidnightIntentService {
@Inject Broadcaster broadcaster;
@Inject RefreshScheduler refreshScheduler;
@Inject TaskDao taskDao;
public RefreshSchedulerIntentService() {
super(RefreshSchedulerIntentService.class.getSimpleName());
}
@Override
void run() {
scheduleApplicationRefreshes();
broadcaster.refresh();
}
public void scheduleApplicationRefreshes() {
long now = currentTimeMillis();
long midnight = nextMidnight(now);
Criterion criterion = Criterion.or(
Criterion.and(Task.HIDE_UNTIL.gt(now), Task.HIDE_UNTIL.lt(midnight)),
Criterion.and(Task.DUE_DATE.gt(now), Task.DUE_DATE.lt(midnight)));
taskDao.selectActive(criterion, refreshScheduler::scheduleRefresh);
}
@Override
protected void inject(IntentServiceComponent component) {
component.inject(this);
}
}

@ -0,0 +1,49 @@
package org.tasks.scheduling;
import android.content.Intent;
import com.todoroo.andlib.sql.Criterion;
import com.todoroo.astrid.dao.TaskDao;
import com.todoroo.astrid.data.Task;
import org.tasks.injection.InjectingIntentService;
import org.tasks.injection.IntentServiceComponent;
import org.tasks.jobs.JobManager;
import javax.inject.Inject;
import timber.log.Timber;
import static java.lang.System.currentTimeMillis;
public class SchedulerIntentService extends InjectingIntentService {
@Inject TaskDao taskDao;
@Inject JobManager jobManager;
@Inject RefreshScheduler refreshScheduler;
public SchedulerIntentService() {
super(SchedulerIntentService.class.getSimpleName());
}
@Override
protected void onHandleIntent(Intent intent) {
super.onHandleIntent(intent);
Timber.d("onHandleIntent(%s)", intent);
jobManager.scheduleMidnightBackup(true);
jobManager.scheduleMidnightRefresh(true);
refreshScheduler.clear();
long now = currentTimeMillis();
taskDao.selectActive(
Criterion.or(Task.HIDE_UNTIL.gt(now), Task.DUE_DATE.gt(now)),
refreshScheduler::scheduleRefresh);
}
@Override
protected void inject(IntentServiceComponent component) {
component.inject(this);
}
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="sharedpref" path="evernote_jobs.xml" />
<exclude domain="database" path="evernote_jobs.db" />
</full-backup-content>
Loading…
Cancel
Save