From bfb0313f36acc0f3f96d0cfe26a8bffa034d900b Mon Sep 17 00:00:00 2001 From: Tim Su Date: Sat, 27 Dec 2008 20:26:12 +0000 Subject: [PATCH] Adding snoozing, notifications for upcoming deadlines, and made the system wake you when an alarm hits. --- AndroidManifest.xml | 4 +- res/values/strings.xml | 8 +- src/com/timsu/astrid/activities/TaskEdit.java | 2 +- src/com/timsu/astrid/activities/TaskView.java | 3 +- .../astrid/data/task/TaskController.java | 22 +++ .../astrid/data/task/TaskModelForNotify.java | 12 ++ .../timsu/astrid/utilities/Notifications.java | 169 +++++++++++++----- 7 files changed, 174 insertions(+), 46 deletions(-) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 6d30d11cd..f0b2b7b48 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="7" + android:versionName="1.7.1"> diff --git a/res/values/strings.xml b/res/values/strings.xml index 9da25c26c..11712e2a2 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -157,10 +157,14 @@ Remove this tag from all tasks? Stop the timer? + + + - Next notification time: + Absolute Deadline Coming Up! + Goal Deadline Coming Up! - + Couldn't find this item: diff --git a/src/com/timsu/astrid/activities/TaskEdit.java b/src/com/timsu/astrid/activities/TaskEdit.java index 11424e1b0..8907da344 100644 --- a/src/com/timsu/astrid/activities/TaskEdit.java +++ b/src/com/timsu/astrid/activities/TaskEdit.java @@ -199,7 +199,7 @@ public class TaskEdit extends TaskModificationActivity { } // set up notification - Notifications.updateAlarm(this, model); + Notifications.updateAlarm(this, model, true); } /** Save task tags. Must be called after task already has an ID */ diff --git a/src/com/timsu/astrid/activities/TaskView.java b/src/com/timsu/astrid/activities/TaskView.java index 948ab8c31..b30b84479 100644 --- a/src/com/timsu/astrid/activities/TaskView.java +++ b/src/com/timsu/astrid/activities/TaskView.java @@ -53,6 +53,7 @@ public class TaskView extends TaskModificationActivity { // bundle tokens public static final String FROM_NOTIFICATION_TOKEN = "notify"; + public static final String NOTIF_FLAGS_TOKEN = "notif_flags"; // activities private static final int ACTIVITY_EDIT = 0; @@ -108,7 +109,7 @@ public class TaskView extends TaskModificationActivity { // clear residual, schedule the next one Notifications.clearAllNotifications(this, model.getTaskIdentifier()); - Notifications.updateAlarm(this, model); + Notifications.updateAlarm(this, model, true); String[] responses = r.getStringArray(R.array.reminder_responses); String response = responses[new Random().nextInt(responses.length)]; diff --git a/src/com/timsu/astrid/data/task/TaskController.java b/src/com/timsu/astrid/data/task/TaskController.java index 06aad1ac9..2539ca96b 100644 --- a/src/com/timsu/astrid/data/task/TaskController.java +++ b/src/com/timsu/astrid/data/task/TaskController.java @@ -61,6 +61,28 @@ public class TaskController extends AbstractController { return list; } + /** Return a list of all active tasks with deadlines */ + public List getTasksWithDeadlines() { + List list = new ArrayList(); + Cursor cursor = database.query(TASK_TABLE_NAME, TaskModelForNotify.FIELD_LIST, + String.format("%s < %d AND (%s != 0 OR %s != 0)", + AbstractTaskModel.PROGRESS_PERCENTAGE, + AbstractTaskModel.COMPLETE_PERCENTAGE, + AbstractTaskModel.DEFINITE_DUE_DATE, + AbstractTaskModel.PREFERRED_DUE_DATE), null, null, null, null, null); + + if(cursor.getCount() == 0) + return list; + + do { + cursor.moveToNext(); + list.add(new TaskModelForNotify(cursor)); + } while(!cursor.isLast()); + + cursor.close(); + return list; + } + /** Return a list of all of the tasks with progress < COMPLETE_PERCENTAGE */ public Cursor getActiveTaskListCursor() { return database.query(TASK_TABLE_NAME, TaskModelForList.FIELD_LIST, diff --git a/src/com/timsu/astrid/data/task/TaskModelForNotify.java b/src/com/timsu/astrid/data/task/TaskModelForNotify.java index 9d3da6788..bf5a20a2b 100644 --- a/src/com/timsu/astrid/data/task/TaskModelForNotify.java +++ b/src/com/timsu/astrid/data/task/TaskModelForNotify.java @@ -36,6 +36,8 @@ public class TaskModelForNotify extends AbstractTaskModel implements Notifiable NOTIFICATIONS, HIDDEN_UNTIL, PROGRESS_PERCENTAGE, + DEFINITE_DUE_DATE, + PREFERRED_DUE_DATE, }; // --- constructors @@ -63,4 +65,14 @@ public class TaskModelForNotify extends AbstractTaskModel implements Notifiable public Date getHiddenUntil() { return super.getHiddenUntil(); } + + @Override + public Date getDefiniteDueDate() { + return super.getDefiniteDueDate(); + } + + @Override + public Date getPreferredDueDate() { + return super.getPreferredDueDate(); + } } diff --git a/src/com/timsu/astrid/utilities/Notifications.java b/src/com/timsu/astrid/utilities/Notifications.java index 4536adf6d..5a6200047 100644 --- a/src/com/timsu/astrid/utilities/Notifications.java +++ b/src/com/timsu/astrid/utilities/Notifications.java @@ -24,13 +24,25 @@ import com.timsu.astrid.data.task.TaskModelForNotify; public class Notifications extends BroadcastReceiver { - private static final String ID_KEY = "id"; - private static final int MIN_INTERVAL_SECONDS = 300; - - private static final float FUDGE_MIN = 0.2f; - private static final float FUDGE_MAX = 0.8f; - - private static Random random = new Random(); + private static final String ID_KEY = "id"; + private static final String FLAGS_KEY = "flags"; + + // stuff for scheduling + private static final int MIN_INTERVAL_SECONDS = 300; + private static final float FUDGE_MIN = 0.2f; + private static final float FUDGE_MAX = 0.8f; + /** # of seconds before a deadline to notify */ + private static final int DEADLINE_NOTIFY_SECS = 3600; + /** # of seconds after now, if a deadline is in the past */ + private static final int TIME_IN_PAST_OFFSET = 60; + /** # of seconds after first deadline reminder to repeat */ + private static final int DEADLINE_REPEAT = 600; + + // flags + public static final int FLAG_DEFINITE_DEADLINE = 1; + public static final int FLAG_PREFERRED_DEADLINE = 2; + + private static Random random = new Random(); /** Something we can create a notification for */ public interface Notifiable { @@ -38,14 +50,27 @@ public class Notifications extends BroadcastReceiver { public Integer getNotificationIntervalSeconds(); public boolean isTaskCompleted(); public Date getHiddenUntil(); + public Date getDefiniteDueDate(); + public Date getPreferredDueDate(); } @Override /** Alarm intent */ public void onReceive(Context context, Intent intent) { long id = intent.getLongExtra(ID_KEY, 0); + int flags = intent.getIntExtra(FLAGS_KEY, 0); Log.e("ALARM", "Alarm triggered id " + id); - if(!showNotification(context, id)) { + + Resources r = context.getResources(); + String reminder; + if((flags & FLAG_DEFINITE_DEADLINE) > 0) + reminder = r.getString(R.string.notif_definiteDueDate); + else if((flags & FLAG_PREFERRED_DEADLINE) > 0) + reminder = r.getString(R.string.notif_preferredDueDate); + else + reminder = getRandomReminder(r); + + if(!showNotification(context, id, flags, reminder)) { deleteAlarm(context, id); NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); @@ -55,11 +80,7 @@ public class Notifications extends BroadcastReceiver { // --- alarm manager stuff - private static boolean isAlarmEnabled(Notifiable task) { - if(task.getNotificationIntervalSeconds() == null || - task.getNotificationIntervalSeconds() == 0) - return false; - + private static boolean shouldDeleteAlarm(Notifiable task) { if(task.isTaskCompleted()) return false; @@ -72,37 +93,84 @@ public class Notifications extends BroadcastReceiver { List tasks = controller.getTasksWithNotifications(); for(TaskModelForNotify task : tasks) - updateAlarm(context, task); + updateAlarm(context, task, false); controller.close(); } /** Schedules the next notification for this task */ - public static void updateAlarm(Context context, Notifiable task) { + public static void updateAlarm(Context context, Notifiable task, + boolean shouldSnooze) { if(task.getTaskIdentifier() == null) return; - if(!isAlarmEnabled(task)) { + if(!shouldDeleteAlarm(task)) { + deleteAlarm(context, task.getTaskIdentifier().getId()); + return; + } + + Long when = null; // when to schedule alarm (ms) + Integer interval = null; // how often to repeat (s) + + if(task.getNotificationIntervalSeconds() > 0) { + // compute, and add a fudge factor to mix things up a bit + interval = task.getNotificationIntervalSeconds(); + int currentSeconds = (int)(System.currentTimeMillis() / 1000); + int untilNextInterval = interval - currentSeconds % interval; + untilNextInterval = 60; + untilNextInterval *= FUDGE_MIN + random.nextFloat() * (FUDGE_MAX - FUDGE_MIN); + if(untilNextInterval < MIN_INTERVAL_SECONDS) + untilNextInterval = MIN_INTERVAL_SECONDS; + when = System.currentTimeMillis() + untilNextInterval * 1000; + } + + // if deadlines come before, do that instead + int flags = 0; + if(task.getDefiniteDueDate() != null) { + long deadlineWhen = task.getDefiniteDueDate().getTime() - + DEADLINE_NOTIFY_SECS * 1000; + if(when == null || deadlineWhen < when) { + when = deadlineWhen; + interval = DEADLINE_REPEAT; + flags = FLAG_DEFINITE_DEADLINE; + } + } + if(task.getPreferredDueDate() != null) { + long deadlineWhen = task.getPreferredDueDate().getTime() - + DEADLINE_NOTIFY_SECS * 1000; + if(when == null || deadlineWhen < when) { + when = deadlineWhen; + interval = DEADLINE_REPEAT; + flags = FLAG_PREFERRED_DEADLINE; + } + } + + if(when == null) { deleteAlarm(context, task.getTaskIdentifier().getId()); return; } - // compute, and add a fudge factor to mix things up a bit - int interval = task.getNotificationIntervalSeconds(); - int currentSeconds = (int)(System.currentTimeMillis() / 1000); - int untilNextInterval = interval - currentSeconds % interval; - untilNextInterval *= FUDGE_MIN + random.nextFloat() * (FUDGE_MAX - FUDGE_MIN); - if(untilNextInterval < MIN_INTERVAL_SECONDS) - untilNextInterval = MIN_INTERVAL_SECONDS; - long when = System.currentTimeMillis() + untilNextInterval * 1000; - scheduleAlarm(context, task.getTaskIdentifier().getId(), when, - interval*1000); + // snooze if the user just interacted with this item + if(shouldSnooze) { + long snoozeWhen = System.currentTimeMillis() + DEADLINE_REPEAT; + if(when < snoozeWhen) + when = snoozeWhen; + } else if(when < System.currentTimeMillis()) + when = System.currentTimeMillis() + TIME_IN_PAST_OFFSET*1000; + + if(interval == null) + scheduleAlarm(context, task.getTaskIdentifier().getId(), when, flags); + else + scheduleRepeatingAlarm(context, task.getTaskIdentifier().getId(), + when, flags, interval*1000); } - private static PendingIntent createPendingIntent(Context context, long id) { + private static PendingIntent createPendingIntent(Context context, + long id, int flags) { Intent intent = new Intent(context, Notifications.class); intent.setType(Long.toString(id)); intent.putExtra(ID_KEY, id); + intent.putExtra(FLAGS_KEY, flags); PendingIntent sender = PendingIntent.getBroadcast(context, 0, intent, 0); return sender; @@ -112,15 +180,26 @@ public class Notifications extends BroadcastReceiver { public static void deleteAlarm(Context context, long id) { AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - am.cancel(createPendingIntent(context, id)); + am.cancel(createPendingIntent(context, id, 0)); } - /** Schedules a recurring alarm for a single task */ - public static void scheduleAlarm(Context context, long id, long when, long interval) { + /** Schedules a single alarm for a single task */ + public static void scheduleAlarm(Context context, long id, long when, + int flags) { AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); Log.e("ALARM", "Alarm set for " + new Date(when)); - am.setRepeating(AlarmManager.RTC, when, interval, createPendingIntent(context, id)); + am.set(AlarmManager.RTC_WAKEUP, when, createPendingIntent(context, id, flags)); + } + + /** Schedules a recurring alarm for a single task */ + public static void scheduleRepeatingAlarm(Context context, long id, long when, + int flags, long interval) { + AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + + Log.e("ALARM", "Alarm set for " + new Date(when) + " every " + interval); + am.setRepeating(AlarmManager.RTC_WAKEUP, when, interval, + createPendingIntent(context, id, flags)); } // --- notification manager stuff @@ -132,30 +211,42 @@ public class Notifications extends BroadcastReceiver { nm.cancel((int)taskId.getId()); } + private static String getRandomReminder(Resources r) { + String[] reminders = r.getStringArray(R.array.reminders); + int next = random.nextInt(reminders.length); + String reminder = reminders[next]; + return reminder; + } + /** Schedule a new notification about the given task. Returns false if there was * some sort of error or the alarm should be disabled. */ - public static boolean showNotification(Context context, long id) { + public static boolean showNotification(Context context, long id, int flags, + String reminder) { + String taskName; TaskController controller = new TaskController(context); try { controller.open(); TaskModelForList task = controller.fetchTaskForList(new TaskIdentifier(id)); - // you're working on it - don't delete + // you're working on it - don't sound, don't delete if(task.getTimerStart() != null) return true; - // you're done - delete + // you're done - don't sound, do delete if(task.isTaskCompleted()) return false; - // it's hidden - don't delete + // it's hidden - don't sound, do don't delete if(task.getHiddenUntil() != null && task.getHiddenUntil().after(new Date())) return true; + taskName = task.getName(); + } catch (Exception e) { // task could be deleted, for example - Log.e(Notifications.class.getSimpleName(), "Error loading task", e); + Log.e(Notifications.class.getSimpleName(), + "Error loading task for notification", e); return false; } finally { controller.close(); @@ -168,21 +259,19 @@ public class Notifications extends BroadcastReceiver { Intent notifyIntent = new Intent(context, TaskView.class); notifyIntent.putExtra(TaskView.LOAD_INSTANCE_TOKEN, id); notifyIntent.putExtra(TaskView.FROM_NOTIFICATION_TOKEN, true); + notifyIntent.putExtra(TaskView.NOTIF_FLAGS_TOKEN, flags); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_ONE_SHOT); // notification text String appName = r.getString(R.string.app_name); - String[] reminders = r.getStringArray(R.array.reminders); - int next = random.nextInt(reminders.length); - String reminder = reminders[next]; Notification notification = new Notification( android.R.drawable.stat_notify_chat, reminder, System.currentTimeMillis()); notification.setLatestEventInfo(context, - appName, + appName + ": " + taskName, reminder, pendingIntent);