mirror of https://github.com/tasks/tasks
Merge remote-tracking branch 'upstream/master'
# Conflicts: # src/main/res/values/keys.xml # src/main/res/xml/preferences_gtasks.xmlgtask_note_sync
commit
0b194a6cf9
@ -1,6 +1,6 @@
|
||||
#Thu Jan 26 17:04:55 CST 2017
|
||||
#Tue May 23 14:22:03 CDT 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-milestone-1-all.zip
|
||||
|
@ -1,72 +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.getContext;
|
||||
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(getContext(), 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));
|
||||
}
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
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.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 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.time.DateTimeUtils.currentTimeMillis;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class JobQueueTest {
|
||||
|
||||
private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
|
||||
private static final String TAG = "test";
|
||||
|
||||
private JobQueue<Reminder> queue;
|
||||
private JobManager jobManager;
|
||||
private Preferences preferences;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
preferences = mock(Preferences.class);
|
||||
when(preferences.adjustForQuietHours(anyLong())).then(returnsFirstArg());
|
||||
jobManager = mock(JobManager.class);
|
||||
queue = new JobQueue<>(preferences, jobManager, TAG);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
verifyNoMoreInteractions(jobManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoJobsAtSameTime() {
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
queue.add(new Reminder(2, 1, 0));
|
||||
|
||||
verify(jobManager).schedule(TAG, 1);
|
||||
|
||||
assertEquals(2, queue.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescheduleForFirstJob() {
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
|
||||
verify(jobManager).schedule(TAG, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontRescheduleForLaterJobs() {
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
queue.add(new Reminder(2, 2, 0));
|
||||
|
||||
verify(jobManager).schedule(TAG, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescheduleForNewerJob() {
|
||||
queue.add(new Reminder(1, 2, 0));
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
|
||||
InOrder order = inOrder(jobManager);
|
||||
order.verify(jobManager).schedule(TAG, 2);
|
||||
order.verify(jobManager).schedule(TAG, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescheduleWhenCancelingOnlyJob() {
|
||||
queue.add(new Reminder(1, 2, 0));
|
||||
queue.cancel(1);
|
||||
|
||||
InOrder order = inOrder(jobManager);
|
||||
order.verify(jobManager).schedule(TAG, 2);
|
||||
order.verify(jobManager).cancel(TAG);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rescheduleWhenCancelingFirstJob() {
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
queue.add(new Reminder(2, 2, 0));
|
||||
|
||||
queue.cancel(1);
|
||||
|
||||
InOrder order = inOrder(jobManager);
|
||||
order.verify(jobManager).schedule(TAG, 1);
|
||||
order.verify(jobManager).schedule(TAG, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dontRescheduleWhenCancelingLaterJob() {
|
||||
queue.add(new Reminder(1, 1, 0));
|
||||
queue.add(new Reminder(2, 2, 0));
|
||||
|
||||
queue.cancel(2);
|
||||
|
||||
verify(jobManager).schedule(TAG, 1);
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
verify(jobManager).schedule(TAG, 1234);
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
verify(jobManager).schedule(TAG, now);
|
||||
|
||||
Freeze.freezeAt(now).thawAfter(new Snippet() {{
|
||||
assertEquals(
|
||||
singletonList(new Reminder(1, now, TYPE_DUE)),
|
||||
queue.getOverdueJobs());
|
||||
}});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoOverdueJobsAtSameTimeReturned() {
|
||||
long now = currentTimeMillis();
|
||||
|
||||
queue.add(new Reminder(1, now, TYPE_DUE));
|
||||
queue.add(new Reminder(2, now, TYPE_DUE));
|
||||
|
||||
verify(jobManager).schedule(TAG, now);
|
||||
|
||||
Freeze.freezeAt(now).thawAfter(new Snippet() {{
|
||||
assertEquals(
|
||||
asList(new Reminder(1, now, TYPE_DUE), new Reminder(2, now, TYPE_DUE)),
|
||||
queue.getOverdueJobs());
|
||||
}});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoOverdueJobsAtDifferentTimes() {
|
||||
long now = currentTimeMillis();
|
||||
|
||||
queue.add(new Reminder(1, now, TYPE_DUE));
|
||||
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
|
||||
|
||||
verify(jobManager).schedule(TAG, now);
|
||||
|
||||
Freeze.freezeAt(now + 2 * ONE_MINUTE).thawAfter(new Snippet() {{
|
||||
assertEquals(
|
||||
asList(new Reminder(1, now, TYPE_DUE), new Reminder(2, now + ONE_MINUTE, TYPE_DUE)),
|
||||
queue.getOverdueJobs());
|
||||
}});
|
||||
}
|
||||
|
||||
@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));
|
||||
|
||||
verify(jobManager).schedule(TAG, now);
|
||||
|
||||
Freeze.freezeAt(now).thawAfter(new Snippet() {{
|
||||
queue.remove(new Reminder(1, now, TYPE_DUE));
|
||||
}});
|
||||
|
||||
assertEquals(
|
||||
singletonList(new Reminder(2, now + ONE_MINUTE, TYPE_DUE)),
|
||||
queue.getJobs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multipleOverduePeriodsLapsed() {
|
||||
long now = currentTimeMillis();
|
||||
|
||||
queue.add(new Reminder(1, now, TYPE_DUE));
|
||||
queue.add(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
|
||||
queue.add(new Reminder(3, now + 2 * ONE_MINUTE, TYPE_DUE));
|
||||
|
||||
verify(jobManager).schedule(TAG, now);
|
||||
|
||||
Freeze.freezeAt(now + ONE_MINUTE).thawAfter(new Snippet() {{
|
||||
queue.remove(new Reminder(1, now, TYPE_DUE));
|
||||
queue.remove(new Reminder(2, now + ONE_MINUTE, TYPE_DUE));
|
||||
}});
|
||||
|
||||
assertEquals(
|
||||
singletonList(new Reminder(3, now + 2 * ONE_MINUTE, TYPE_DUE)),
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package org.tasks.injection;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.tasks.locale.Locale;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
class Dagger {
|
||||
|
||||
private static final Object lock = new Object();
|
||||
|
||||
private static Dagger instance;
|
||||
|
||||
public static Dagger get(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (lock) {
|
||||
if (instance == null) {
|
||||
instance = new Dagger(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ApplicationComponent applicationComponent;
|
||||
|
||||
private Dagger(Context context) {
|
||||
Context localeContext = context.getApplicationContext();
|
||||
try {
|
||||
localeContext = Locale.getInstance(localeContext)
|
||||
.createConfigurationContext(localeContext);
|
||||
} catch (Exception e) {
|
||||
Timber.e(e.getMessage(), e);
|
||||
}
|
||||
|
||||
applicationComponent = DaggerApplicationComponent.builder()
|
||||
.applicationModule(new ApplicationModule(localeContext))
|
||||
.build();
|
||||
}
|
||||
|
||||
ApplicationComponent getApplicationComponent() {
|
||||
return applicationComponent;
|
||||
}
|
||||
}
|
@ -1,25 +1,37 @@
|
||||
package org.tasks.injection;
|
||||
|
||||
import org.tasks.jobs.AlarmJob;
|
||||
import org.tasks.jobs.BackupJob;
|
||||
import org.tasks.jobs.MidnightRefreshJob;
|
||||
import org.tasks.jobs.RefreshJob;
|
||||
import org.tasks.jobs.ReminderJob;
|
||||
import org.tasks.location.GeofenceTransitionsIntentService;
|
||||
import org.tasks.scheduling.BackupIntentService;
|
||||
import org.tasks.scheduling.CalendarNotificationIntentService;
|
||||
import org.tasks.scheduling.GeofenceSchedulingIntentService;
|
||||
import org.tasks.scheduling.RefreshSchedulerIntentService;
|
||||
import org.tasks.scheduling.ReminderSchedulerIntentService;
|
||||
import org.tasks.scheduling.NotificationSchedulerIntentService;
|
||||
import org.tasks.scheduling.SchedulerIntentService;
|
||||
|
||||
import dagger.Subcomponent;
|
||||
|
||||
@Subcomponent(modules = IntentServiceModule.class)
|
||||
public interface IntentServiceComponent {
|
||||
void inject(ReminderSchedulerIntentService reminderSchedulerIntentService);
|
||||
|
||||
void inject(RefreshSchedulerIntentService refreshSchedulerIntentService);
|
||||
void inject(SchedulerIntentService schedulerIntentService);
|
||||
|
||||
void inject(GeofenceSchedulingIntentService geofenceSchedulingIntentService);
|
||||
|
||||
void inject(CalendarNotificationIntentService calendarNotificationIntentService);
|
||||
|
||||
void inject(BackupIntentService backupIntentService);
|
||||
|
||||
void inject(GeofenceTransitionsIntentService geofenceTransitionsIntentService);
|
||||
|
||||
void inject(NotificationSchedulerIntentService notificationSchedulerIntentService);
|
||||
|
||||
void inject(AlarmJob alarmJob);
|
||||
|
||||
void inject(BackupJob backupJob);
|
||||
|
||||
void inject(MidnightRefreshJob midnightRefreshJob);
|
||||
|
||||
void inject(RefreshJob refreshJob);
|
||||
|
||||
void inject(ReminderJob reminderJob);
|
||||
}
|
||||
|
@ -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,56 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
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.injection.IntentServiceComponent;
|
||||
import org.tasks.preferences.Preferences;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class AlarmJob extends WakefulJob {
|
||||
|
||||
public static final String TAG = "job_alarm";
|
||||
|
||||
@Inject Preferences preferences;
|
||||
@Inject AlarmService alarmService;
|
||||
@Inject Notifier notifier;
|
||||
@Inject TaskDao taskDao;
|
||||
|
||||
public AlarmJob() {
|
||||
super(AlarmJob.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
if (!preferences.isCurrentlyQuietHours()) {
|
||||
for (Alarm alarm : alarmService.getOverdueAlarms()) {
|
||||
Task task = taskDao.fetch(alarm.getTaskId(), Task.REMINDER_LAST);
|
||||
if (task != null && task.getReminderLast() < alarm.getTime()) {
|
||||
notifier.triggerTaskNotification(alarm.getTaskId(), ReminderService.TYPE_ALARM);
|
||||
}
|
||||
alarmService.remove(alarm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduleNext() {
|
||||
alarmService.scheduleNextJob();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void inject(IntentServiceComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void completeWakefulIntent(Intent intent) {
|
||||
AlarmJobBroadcast.completeWakefulIntent(intent);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.WakefulBroadcastReceiver;
|
||||
|
||||
public class AlarmJobBroadcast extends WakefulBroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
startWakefulService(context, new Intent(context, AlarmJob.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import org.tasks.analytics.Tracker;
|
||||
import org.tasks.injection.InjectingIntentService;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public abstract class Job extends InjectingIntentService {
|
||||
|
||||
@Inject Tracker tracker;
|
||||
|
||||
public Job(String name) {
|
||||
super(name);
|
||||
setIntentRedelivery(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
super.onHandleIntent(intent);
|
||||
|
||||
Timber.d("onHandleIntent(%s)", intent);
|
||||
|
||||
try {
|
||||
run();
|
||||
} catch (Exception e) {
|
||||
tracker.reportException(e);
|
||||
} finally {
|
||||
scheduleNext();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void run();
|
||||
|
||||
protected abstract void scheduleNext();
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.tasks.injection.ApplicationScope;
|
||||
import org.tasks.injection.ForApplication;
|
||||
import org.tasks.scheduling.AlarmManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
|
||||
import static org.tasks.time.DateTimeUtils.nextMidnight;
|
||||
import static org.tasks.time.DateTimeUtils.printTimestamp;
|
||||
|
||||
@ApplicationScope
|
||||
public class JobManager {
|
||||
|
||||
private Context context;
|
||||
private AlarmManager alarmManager;
|
||||
|
||||
@Inject
|
||||
public JobManager(@ForApplication Context context, AlarmManager alarmManager) {
|
||||
this.context = context;
|
||||
this.alarmManager = alarmManager;
|
||||
}
|
||||
|
||||
public void schedule(String tag, long time) {
|
||||
Timber.d("%s: %s", tag, printTimestamp(time));
|
||||
alarmManager.wakeup(adjust(time), getPendingIntent(tag));
|
||||
}
|
||||
|
||||
public void scheduleRefresh(long time) {
|
||||
Timber.d("%s: %s", RefreshJob.TAG, printTimestamp(time));
|
||||
alarmManager.noWakeup(adjust(time), getPendingService(RefreshJob.class));
|
||||
}
|
||||
|
||||
public void scheduleMidnightRefresh() {
|
||||
long time = nextMidnight();
|
||||
Timber.d("%s: %s", MidnightRefreshJob.TAG, printTimestamp(time));
|
||||
alarmManager.noWakeup(adjust(time), getPendingService(MidnightRefreshJob.class));
|
||||
}
|
||||
|
||||
public void scheduleMidnightBackup() {
|
||||
long time = nextMidnight();
|
||||
Timber.d("%s: %s", BackupJob.TAG, printTimestamp(time));
|
||||
alarmManager.noWakeup(adjust(time), getPendingService(BackupJob.class));
|
||||
}
|
||||
|
||||
public void cancel(String tag) {
|
||||
Timber.d("CXL %s", tag);
|
||||
alarmManager.cancel(getPendingIntent(tag));
|
||||
}
|
||||
|
||||
private long adjust(long time) {
|
||||
return Math.max(time, currentTimeMillis() + 5000);
|
||||
}
|
||||
|
||||
private PendingIntent getPendingIntent(String tag) {
|
||||
switch (tag) {
|
||||
case ReminderJob.TAG:
|
||||
return getPendingBroadcast(ReminderJobBroadcast.class);
|
||||
case AlarmJob.TAG:
|
||||
return getPendingBroadcast(AlarmJobBroadcast.class);
|
||||
case RefreshJob.TAG:
|
||||
return getPendingService(RefreshJob.class);
|
||||
default:
|
||||
throw new RuntimeException("Unexpected tag: " + tag);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> PendingIntent getPendingBroadcast(Class<T> c) {
|
||||
return PendingIntent.getBroadcast(context, 0, new Intent(context, c), 0);
|
||||
}
|
||||
|
||||
private <T> PendingIntent getPendingService(Class<T> c) {
|
||||
return PendingIntent.getService(context, 0, new Intent(context, c), 0);
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSortedSet;
|
||||
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.NavigableSet;
|
||||
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;
|
||||
private final JobManager jobManager;
|
||||
private final String tag;
|
||||
|
||||
public static JobQueue<Reminder> newReminderQueue(Preferences preferences, JobManager jobManager) {
|
||||
return new JobQueue<>(preferences, jobManager, ReminderJob.TAG);
|
||||
}
|
||||
|
||||
public static JobQueue<Alarm> newAlarmQueue(Preferences preferences, JobManager jobManager) {
|
||||
return new JobQueue<>(preferences, jobManager, AlarmJob.TAG);
|
||||
}
|
||||
|
||||
JobQueue(Preferences preferences, JobManager jobManager, String tag) {
|
||||
this.preferences = preferences;
|
||||
this.jobManager = jobManager;
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
public synchronized void add(T entry) {
|
||||
boolean reschedule = jobs.isEmpty() || entry.getTime() < firstTime();
|
||||
jobs.put(entry.getTime(), entry);
|
||||
if (reschedule) {
|
||||
scheduleNext(true);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
jobs.clear();
|
||||
jobManager.cancel(tag);
|
||||
}
|
||||
|
||||
public synchronized void 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);
|
||||
}
|
||||
if (reschedule) {
|
||||
scheduleNext(true);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized List<T> getOverdueJobs() {
|
||||
List<T> result = newArrayList();
|
||||
for (Long key : jobs.keySet().headSet(currentTimeMillis() + 1)) {
|
||||
result.addAll(jobs.get(key));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public synchronized boolean remove(T entry) {
|
||||
return jobs.remove(entry.getTime(), entry);
|
||||
}
|
||||
|
||||
public synchronized void scheduleNext() {
|
||||
scheduleNext(false);
|
||||
}
|
||||
|
||||
private void scheduleNext(boolean cancelCurrent) {
|
||||
if (jobs.isEmpty()) {
|
||||
if (cancelCurrent) {
|
||||
jobManager.cancel(tag);
|
||||
}
|
||||
} else {
|
||||
jobManager.schedule(tag, nextScheduledTime());
|
||||
}
|
||||
}
|
||||
|
||||
private long firstTime() {
|
||||
return jobs.isEmpty() ? 0 : jobs.asMap().firstKey();
|
||||
}
|
||||
|
||||
long nextScheduledTime() {
|
||||
long next = firstTime();
|
||||
return next > 0 ? preferences.adjustForQuietHours(next) : 0;
|
||||
}
|
||||
|
||||
int size() {
|
||||
return jobs.size();
|
||||
}
|
||||
|
||||
List<T> getJobs() {
|
||||
return ImmutableList.copyOf(jobs.values());
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
public interface JobQueueEntry {
|
||||
long getId();
|
||||
|
||||
long getTime();
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
public abstract class MidnightJob extends Job {
|
||||
public MidnightJob(String name) {
|
||||
super(name);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import org.tasks.Broadcaster;
|
||||
import org.tasks.injection.IntentServiceComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class MidnightRefreshJob extends MidnightJob {
|
||||
|
||||
public static final String TAG = "job_midnight_refresh";
|
||||
|
||||
@Inject Broadcaster broadcaster;
|
||||
@Inject JobManager jobManager;
|
||||
|
||||
public MidnightRefreshJob() {
|
||||
super(MidnightRefreshJob.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
broadcaster.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduleNext() {
|
||||
jobManager.scheduleMidnightRefresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void inject(IntentServiceComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import org.tasks.Broadcaster;
|
||||
import org.tasks.injection.IntentServiceComponent;
|
||||
import org.tasks.scheduling.RefreshScheduler;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class RefreshJob extends Job {
|
||||
|
||||
public static final String TAG = "job_refresh";
|
||||
|
||||
@Inject RefreshScheduler refreshScheduler;
|
||||
@Inject Broadcaster broadcaster;
|
||||
|
||||
public RefreshJob() {
|
||||
super(RefreshJob.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void inject(IntentServiceComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
broadcaster.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduleNext() {
|
||||
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,49 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import com.todoroo.astrid.reminders.ReminderService;
|
||||
|
||||
import org.tasks.Notifier;
|
||||
import org.tasks.injection.IntentServiceComponent;
|
||||
import org.tasks.preferences.Preferences;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class ReminderJob extends WakefulJob {
|
||||
|
||||
public static final String TAG = "job_reminder";
|
||||
|
||||
@Inject Preferences preferences;
|
||||
@Inject ReminderService reminderService;
|
||||
@Inject Notifier notifier;
|
||||
|
||||
public ReminderJob() {
|
||||
super(ReminderJob.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void inject(IntentServiceComponent component) {
|
||||
component.inject(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
if (!preferences.isCurrentlyQuietHours()) {
|
||||
for (Reminder reminder : reminderService.getPastReminders()) {
|
||||
notifier.triggerTaskNotification(reminder.getId(), reminder.getType());
|
||||
reminderService.remove(reminder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduleNext() {
|
||||
reminderService.scheduleNextJob();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void completeWakefulIntent(Intent intent) {
|
||||
ReminderJobBroadcast.completeWakefulIntent(intent);
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.support.v4.content.WakefulBroadcastReceiver;
|
||||
|
||||
public class ReminderJobBroadcast extends WakefulBroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
startWakefulService(context, new Intent(context, ReminderJob.class));
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package org.tasks.jobs;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
public abstract class WakefulJob extends Job {
|
||||
|
||||
public WakefulJob(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
super.onHandleIntent(intent);
|
||||
completeWakefulIntent(intent);
|
||||
}
|
||||
|
||||
protected abstract void completeWakefulIntent(Intent intent);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.tasks.reminders;
|
||||
|
||||
public class Random {
|
||||
|
||||
private static final java.util.Random random = new java.util.Random();
|
||||
|
||||
public float nextFloat() {
|
||||
return random.nextFloat();
|
||||
}
|
||||
}
|
@ -1,61 +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);
|
||||
|
||||
long lastRun = preferences.getLong(getLastRunPreference(), 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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
jobManager.scheduleMidnightRefresh();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue