package com.orgzly.android.reminders; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.support.v4.app.NotificationCompat; import android.support.v4.content.ContextCompat; import android.util.Log; import com.evernote.android.job.JobManager; import com.evernote.android.job.JobRequest; import com.orgzly.BuildConfig; import com.orgzly.R; import com.orgzly.android.Notifications; import com.orgzly.android.prefs.AppPreferences; import com.orgzly.android.provider.ProviderContract; import com.orgzly.android.ui.util.ActivityUtils; import com.orgzly.android.util.LogUtils; import com.orgzly.org.datetime.OrgDateTime; import com.orgzly.org.datetime.OrgDateTimeUtils; import org.joda.time.DateTime; import org.joda.time.ReadableInstant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; /** * Everything related to reminders goes through this service. * * FIXME: Work in progress, logic below is incomplete and/or broken. */ public class ReminderService extends IntentService { public static final String TAG = ReminderService.class.getName(); public static final String EXTRA_EVENT = "event"; public static final int EVENT_DATA_CHANGED = 1; public static final int EVENT_JOB_TRIGGERED = 2; public static final int EVENT_UNKNOWN = -1; public ReminderService() { super(TAG); setIntentRedelivery(true); } @Override protected void onHandleIntent(Intent intent) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, intent); if (! AppPreferences.remindersForScheduledTimes(this)) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Reminders are disabled"); return; } DateTime now = new DateTime(); /* Previous run time. */ DateTime prevRun = null; long prevRunMillis = AppPreferences.reminderServiceLastRun(this); if (prevRunMillis > 0) { prevRun = new DateTime(prevRunMillis); } int event = intent.getIntExtra(EXTRA_EVENT, EVENT_UNKNOWN); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Event: " + event); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, " Prev: " + prevRun); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, " Now: " + now); switch (event) { case EVENT_DATA_CHANGED: onDataChanged(now, prevRun); break; case EVENT_JOB_TRIGGERED: onJobTriggered(now, prevRun); break; default: Log.e(TAG, "Unknown event received, ignoring it"); return; } AppPreferences.reminderServiceLastRun(this, now.getMillis()); } /** * Schedule the next job for times after last run. */ private void onDataChanged(DateTime now, DateTime prevRun) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, now, prevRun); /* Cancel all jobs. */ JobManager.instance().cancelAllForTag(ReminderJob.TAG); DateTime fromTime = prevRun; if (prevRun == null) { fromTime = now; } scheduleNextJob(now, fromTime); } private void scheduleNextJob(DateTime now, DateTime fromTime) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, now, fromTime); List<NoteWithTime> notes = ReminderService.getNotesWithTimeInInterval(this, fromTime, null); if (! notes.isEmpty()) { /* Schedule only the first upcoming time. */ NoteWithTime firstNote = notes.get(0); /* Schedule *in* exactMs. */ long exactMs = firstNote.time.getMillis() - now.getMillis(); if (exactMs < 0) { exactMs = 1; } int jobId = ReminderJob.scheduleJob(exactMs); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Scheduled for " + jobRunTimeString(jobId) + " (" + firstNote.title + ")"); AppPreferences.reminderServiceJobId(this, jobId); } else { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "No notes found after " + fromTime); } } private String jobRunTimeString(int jobId) { long jobTime = getJobRunTime(jobId); DateTime dateTime = new DateTime(jobTime); return dateTime.toString(); } /** * Display reminders for all notes with times between * last run and now. Then schedule the next job for times after now. */ private void onJobTriggered(DateTime now, DateTime prevRun) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, now, prevRun); /* Cancel all jobs. */ JobManager.instance().cancelAllForTag(ReminderJob.TAG); if (prevRun != null) { /* Show notifications for all notes with times from previous run until now. */ List<NoteWithTime> notes = ReminderService.getNotesWithTimeInInterval(this, prevRun, now); if (!notes.isEmpty()) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Found " + notes.size() + " notes between " + prevRun + " and " + now); showNotification(this, notes); } else { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "No notes found between " + prevRun + " and " + now); } } else { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "No previous run"); } scheduleNextJob(now, now); } private long getJobRunTime(int jobId) { JobRequest jobRequest = JobManager.instance().getJobRequest(jobId); return jobRequest.getScheduledAt() + jobRequest.getStartMs(); } public static List<NoteWithTime> getNotesWithTimeInInterval( Context context, ReadableInstant fromTime, ReadableInstant beforeTime) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG); List<NoteWithTime> result = new ArrayList<>(); Cursor cursor = context.getContentResolver().query( ProviderContract.Times.ContentUri.times(fromTime.getMillis()), null, null, null, null); if (cursor != null) { try { for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long noteId = cursor.getLong(ProviderContract.Times.ColumnIndex.NOTE_ID); long bookId = cursor.getLong(ProviderContract.Times.ColumnIndex.BOOK_ID); String noteState = cursor.getString(ProviderContract.Times.ColumnIndex.NOTE_STATE); String noteTitle = cursor.getString(ProviderContract.Times.ColumnIndex.NOTE_TITLE); String orgTimestampString = cursor.getString(ProviderContract.Times.ColumnIndex.ORG_TIMESTAMP_STRING); OrgDateTime orgDateTime = OrgDateTime.getInstance(orgTimestampString); /* Skip if it's done-type state. */ if (noteState == null || !AppPreferences.doneKeywordsSet(context).contains(noteState)) { List<DateTime> times = OrgDateTimeUtils.getTimesInInterval( orgDateTime, fromTime, beforeTime, 1); for (DateTime time : times) { if (!orgDateTime.hasTime()) { time = time.plusHours(9); // TODO: Move to preferences } result.add(new NoteWithTime(noteId, bookId, noteTitle, time, orgDateTime)); } } } } finally { cursor.close(); } } if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Fetched times, now sorting " + result.size() + " entries by time..."); /* Sort by time, older first. */ Collections.sort(result, new Comparator<NoteWithTime>() { @Override public int compare(NoteWithTime o1, NoteWithTime o2) { if (o1.time == o2.time) { return 0; } else if (o1.time.isBefore(o2.time)) { return -1; } else { return 1; } } }); if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, "Times sorted, total " + result.size()); return result; } static class NoteWithTime { public long id; public long bookId; public String title; public DateTime time; OrgDateTime orgDateTime; NoteWithTime(long id, long bookId, String title, DateTime time, OrgDateTime orgDateTime) { this.id = id; this.bookId = bookId; this.title = title; this.time = time; this.orgDateTime = orgDateTime; } } public static void showNotification(Context context, List<NoteWithTime> notes) { if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG, context, notes); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); NotificationCompat.Builder builder = new NotificationCompat.Builder(context) .setAutoCancel(true) .setPriority(Notification.PRIORITY_MAX) .setColor(ContextCompat.getColor(context, R.color.notification)) .setSmallIcon(R.drawable.cic_orgzly_notification); /* Set notification sound. */ // Uri alarmSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); // builder.setSound(alarmSound); for (int i = 0; i < notes.size(); i++) { NoteWithTime note = notes.get(i); builder.setContentTitle(note.title); builder.setContentText(context.getString( R.string.scheduled_using_time, note.orgDateTime.toStringWithoutBrackets())); /* Open note on notification click. */ PendingIntent intent = ActivityUtils.mainActivityPendingIntent(context, note.bookId, note.id); builder.setContentIntent(intent); notificationManager.notify(String.valueOf(note.id), Notifications.REMINDER, builder.build()); } } /** Notify reminder service about changes that might affect scheduling of reminders. */ public static void notifyDataChanged(Context context) { Intent intent = new Intent(context, ReminderService.class); intent.putExtra(ReminderService.EXTRA_EVENT, ReminderService.EVENT_DATA_CHANGED); context.startService(intent); } /** Notify ReminderService about the triggered job. */ public static void notifyJobTriggered(Context context) { Intent intent = new Intent(context, ReminderService.class); intent.putExtra(ReminderService.EXTRA_EVENT, ReminderService.EVENT_JOB_TRIGGERED); context.startService(intent); } }