The Reminderer app is a TODO app in need of an alarm system. Unfortunately, as with many things in Android, it’s the simple things that are not simple. Take the alarm system. You can use the built-in AlarmManager, but there are some gotchas:
- Do NOT add all alarms at the same time. That will probably overwhelm the AlarmManager, specially if there are hundreds of alarms. It’s enough to just add the next due alarm or alarms—one entry in the AlarmManager at any given time. (The catch is to make sure that you recalculate the next due alarm every time you add/update/delete/fire an alarm.)
- Setup a BroadcastReceiver to listen for system reboots. The AlarmManager doesn’t automatically reload pending alarms on reboot. You have to do that yourself. (Related to that is handling missed alarms. See below.)
Manage timezone changes.The AlarmManager runs in UTC time and you store alarms in the AlarmManager using UTC time, so you don’t have to worry about timezones. (However, if for whatever reason you change the time on the phone, there will be missed alarms.)
- (Optional) Play the alarm using a Service.
- Grab a wake lock in the alarm’s BroadcastReceiver. The BroadcastReceiver already grabs a wake lock in onReceive and releases it when onReceive finishes. However, if you don’t grab another wake lock, the phone might go back to sleep before the alarm plays.
- (Optional) Setup notifications in the system tray. Follow the Android Design Guidelines.
- (Optional) Show a popup when the alarm plays.
- Respect restricted input mode. The KeyGuard is in restricted input mode when the phone is locked and a password is required. You probably don’t want to show the phone content when the phone is in restricted input mode. (I actually haven’t had a chance to test this.)
Phew! And that’s just stuff that applies to any alarm system.
Reminderer’s Alarm System
Let’s talk about the specifics of the Reminderer app. To implement the “basic” alarm system described above, tasks can be stored in a table with these columns:
| ID | TASK_DESC | DUE_DATE | |-------------+-----------+----------| | primary key | string | date |
Finding the next due task corresponds to two queries in the table:
- Get the first task due on or after NOW.
- Get all tasks with the same due date as the task from step 1.
Reminderer also has to do the following:
- Handle missed tasks. For whatever reason (ex: you turn the phone off), there may be missed tasks. Reminderer can show these as “expired non-complete tasks” (aka overdue tasks) from the main screen. Partial solution: add a column
IS_COMPLETEto the table to tell whether a task is complete. Then query for non-complete tasks due in the past.
- Handle super-lengthy database operations. For whatever reason (ex: the database lives in Dropbox), database operations may take a relatively long time. This may result in incorrectly missed alarms.
- Handle repeatable alarms (ex: “buy milk 8pm repeats every week). This one is a little tricky.
- No, don’t store every occurrence in the database. If a task repeats every hour for ever, don’t store every occurrence. All you have to do is store the next due date.
- (Re)calculate the next due dates of repeatable tasks on reboot.
- Find any missed repeatable alarms on reboot.
- What about the table? Create another table to store repeatable tasks and their most recent due date. From an implementation point of view, using two tables is more complex but each table does one thing and one thing well (KISS).
The tables now look like this:
#+CAPTION: Task table | ID | TASK_DESC | DUE_DATE | IS_COMPLETE | REPEATABLE_ID_FK | |-------------+-----------+----------+-------------+------------------| | primary key | string | date | boolean | foreign key | #+CAPTION: Repeatable tasks table | ID | NEXT_DUE_DATE | | primary key | date |
That’s it for now. You can check out the progress over in the newAlarmSystem branch.