Alarms.java revision a3aba0c93c31e89045cca669418e85acfb4e22fb
1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.deskclock;
18
19import android.app.AlarmManager;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.ContentResolver;
23import android.content.ContentValues;
24import android.content.ContentUris;
25import android.content.Context;
26import android.content.Intent;
27import android.content.SharedPreferences;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.Parcel;
31import android.provider.Settings;
32import android.text.format.DateFormat;
33
34import java.util.Calendar;
35import java.text.DateFormatSymbols;
36
37/**
38 * The Alarms provider supplies info about Alarm Clock settings
39 */
40public class Alarms {
41
42    // This action triggers the AlarmReceiver as well as the AlarmKlaxon. It
43    // is a public action used in the manifest for receiving Alarm broadcasts
44    // from the alarm manager.
45    public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
46
47    // A public action sent by AlarmKlaxon when the alarm has stopped sounding
48    // for any reason (e.g. because it has been dismissed from AlarmAlertFullScreen,
49    // or killed due to an incoming phone call, etc).
50    public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
51
52    // AlarmAlertFullScreen listens for this broadcast intent, so that other applications
53    // can snooze the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
54    public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
55
56    // AlarmAlertFullScreen listens for this broadcast intent, so that other applications
57    // can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
58    public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
59
60    // This is a private action used by the AlarmKlaxon to update the UI to
61    // show the alarm has been killed.
62    public static final String ALARM_KILLED = "alarm_killed";
63
64    // Extra in the ALARM_KILLED intent to indicate to the user how long the
65    // alarm played before being killed.
66    public static final String ALARM_KILLED_TIMEOUT = "alarm_killed_timeout";
67
68    // This string is used to indicate a silent alarm in the db.
69    public static final String ALARM_ALERT_SILENT = "silent";
70
71    // This intent is sent from the notification when the user cancels the
72    // snooze alert.
73    public static final String CANCEL_SNOOZE = "cancel_snooze";
74
75    // This string is used when passing an Alarm object through an intent.
76    public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm";
77
78    // This extra is the raw Alarm object data. It is used in the
79    // AlarmManagerService to avoid a ClassNotFoundException when filling in
80    // the Intent extras.
81    public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw";
82
83    // This string is used to identify the alarm id passed to SetAlarm from the
84    // list of alarms.
85    public static final String ALARM_ID = "alarm_id";
86
87    final static String PREF_SNOOZE_ID = "snooze_id";
88    final static String PREF_SNOOZE_TIME = "snooze_time";
89
90    private final static String DM12 = "E h:mm aa";
91    private final static String DM24 = "E k:mm";
92
93    private final static String M12 = "h:mm aa";
94    // Shared with DigitalClock
95    final static String M24 = "kk:mm";
96
97    /**
98     * Creates a new Alarm.
99     */
100    public static long addAlarm(Context context, Alarm alarm) {
101        ContentValues values = createContentValues(alarm);
102        context.getContentResolver().insert(Alarm.Columns.CONTENT_URI, values);
103
104        long timeInMillis = calculateAlarm(alarm);
105        if (alarm.enabled) {
106            clearSnoozeIfNeeded(context, timeInMillis);
107        }
108        setNextAlert(context);
109        return timeInMillis;
110    }
111
112    /**
113     * Removes an existing Alarm.  If this alarm is snoozing, disables
114     * snooze.  Sets next alert.
115     */
116    public static void deleteAlarm(
117            Context context, int alarmId) {
118
119        ContentResolver contentResolver = context.getContentResolver();
120        /* If alarm is snoozing, lose it */
121        disableSnoozeAlert(context, alarmId);
122
123        Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
124        contentResolver.delete(uri, "", null);
125
126        setNextAlert(context);
127    }
128
129    /**
130     * Queries all alarms
131     * @return cursor over all alarms
132     */
133    public static Cursor getAlarmsCursor(ContentResolver contentResolver) {
134        return contentResolver.query(
135                Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS,
136                null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
137    }
138
139    // Private method to get a more limited set of alarms from the database.
140    private static Cursor getFilteredAlarmsCursor(
141            ContentResolver contentResolver) {
142        return contentResolver.query(Alarm.Columns.CONTENT_URI,
143                Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED,
144                null, null);
145    }
146
147    private static ContentValues createContentValues(Alarm alarm) {
148        ContentValues values = new ContentValues(8);
149        // Set the alarm_time value if this alarm does not repeat. This will be
150        // used later to disable expire alarms.
151        long time = 0;
152        if (!alarm.daysOfWeek.isRepeatSet()) {
153            time = calculateAlarm(alarm);
154        }
155
156        values.put(Alarm.Columns.ENABLED, alarm.enabled ? 1 : 0);
157        values.put(Alarm.Columns.HOUR, alarm.hour);
158        values.put(Alarm.Columns.MINUTES, alarm.minutes);
159        values.put(Alarm.Columns.ALARM_TIME, alarm.time);
160        values.put(Alarm.Columns.DAYS_OF_WEEK, alarm.daysOfWeek.getCoded());
161        values.put(Alarm.Columns.VIBRATE, alarm.vibrate);
162        values.put(Alarm.Columns.MESSAGE, alarm.label);
163
164        // A null alert Uri indicates a silent alarm.
165        values.put(Alarm.Columns.ALERT, alarm.alert == null ? ALARM_ALERT_SILENT
166                : alarm.alert.toString());
167
168        return values;
169    }
170
171    private static void clearSnoozeIfNeeded(Context context, long alarmTime) {
172        // If this alarm fires before the next snooze, clear the snooze to
173        // enable this alarm.
174        SharedPreferences prefs =
175                context.getSharedPreferences(AlarmClock.PREFERENCES, 0);
176        long snoozeTime = prefs.getLong(PREF_SNOOZE_TIME, 0);
177        if (alarmTime < snoozeTime) {
178            clearSnoozePreference(context, prefs);
179        }
180    }
181
182    /**
183     * Return an Alarm object representing the alarm id in the database.
184     * Returns null if no alarm exists.
185     */
186    public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) {
187        Cursor cursor = contentResolver.query(
188                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId),
189                Alarm.Columns.ALARM_QUERY_COLUMNS,
190                null, null, null);
191        Alarm alarm = null;
192        if (cursor != null) {
193            if (cursor.moveToFirst()) {
194                alarm = new Alarm(cursor);
195            }
196            cursor.close();
197        }
198        return alarm;
199    }
200
201
202    /**
203     * A convenience method to set an alarm in the Alarms
204     * content provider.
205     * @return Time when the alarm will fire.
206     */
207    public static long setAlarm(Context context, Alarm alarm) {
208        ContentValues values = createContentValues(alarm);
209        ContentResolver resolver = context.getContentResolver();
210        resolver.update(
211                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarm.id),
212                values, null, null);
213
214        long timeInMillis = calculateAlarm(alarm);
215
216        if (alarm.enabled) {
217            // Disable the snooze if we just changed the snoozed alarm. This
218            // only does work if the snoozed alarm is the same as the given
219            // alarm.
220            // TODO: disableSnoozeAlert should have a better name.
221            disableSnoozeAlert(context, alarm.id);
222
223            // Disable the snooze if this alarm fires before the snoozed alarm.
224            // This works on every alarm since the user most likely intends to
225            // have the modified alarm fire next.
226            clearSnoozeIfNeeded(context, timeInMillis);
227        }
228
229        setNextAlert(context);
230
231        return timeInMillis;
232    }
233
234    /**
235     * A convenience method to enable or disable an alarm.
236     *
237     * @param id             corresponds to the _id column
238     * @param enabled        corresponds to the ENABLED column
239     */
240
241    public static void enableAlarm(
242            final Context context, final int id, boolean enabled) {
243        enableAlarmInternal(context, id, enabled);
244        setNextAlert(context);
245    }
246
247    private static void enableAlarmInternal(final Context context,
248            final int id, boolean enabled) {
249        enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
250                enabled);
251    }
252
253    private static void enableAlarmInternal(final Context context,
254            final Alarm alarm, boolean enabled) {
255        if (alarm == null) {
256            return;
257        }
258        ContentResolver resolver = context.getContentResolver();
259
260        ContentValues values = new ContentValues(2);
261        values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
262
263        // If we are enabling the alarm, calculate alarm time since the time
264        // value in Alarm may be old.
265        if (enabled) {
266            long time = 0;
267            if (!alarm.daysOfWeek.isRepeatSet()) {
268                time = calculateAlarm(alarm);
269            }
270            values.put(Alarm.Columns.ALARM_TIME, time);
271        } else {
272            // Clear the snooze if the id matches.
273            disableSnoozeAlert(context, alarm.id);
274        }
275
276        resolver.update(ContentUris.withAppendedId(
277                Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
278    }
279
280    public static Alarm calculateNextAlert(final Context context) {
281        Alarm alarm = null;
282        long minTime = Long.MAX_VALUE;
283        long now = System.currentTimeMillis();
284        Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver());
285        if (cursor != null) {
286            if (cursor.moveToFirst()) {
287                do {
288                    Alarm a = new Alarm(cursor);
289                    // A time of 0 indicates this is a repeating alarm, so
290                    // calculate the time to get the next alert.
291                    if (a.time == 0) {
292                        a.time = calculateAlarm(a);
293                    } else if (a.time < now) {
294                        // Expired alarm, disable it and move along.
295                        enableAlarmInternal(context, a, false);
296                        continue;
297                    }
298                    if (a.time < minTime) {
299                        minTime = a.time;
300                        alarm = a;
301                    }
302                } while (cursor.moveToNext());
303            }
304            cursor.close();
305        }
306        return alarm;
307    }
308
309    /**
310     * Disables non-repeating alarms that have passed.  Called at
311     * boot.
312     */
313    public static void disableExpiredAlarms(final Context context) {
314        Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
315        long now = System.currentTimeMillis();
316
317        if (cur.moveToFirst()) {
318            do {
319                Alarm alarm = new Alarm(cur);
320                // A time of 0 means this alarm repeats. If the time is
321                // non-zero, check if the time is before now.
322                if (alarm.time != 0 && alarm.time < now) {
323                    if (Log.LOGV) {
324                        Log.v("** DISABLE " + alarm.id + " now " + now +" set "
325                                + alarm.time);
326                    }
327                    enableAlarmInternal(context, alarm, false);
328                }
329            } while (cur.moveToNext());
330        }
331        cur.close();
332    }
333
334    /**
335     * Called at system startup, on time/timezone change, and whenever
336     * the user changes alarm settings.  Activates snooze if set,
337     * otherwise loads all alarms, activates next alert.
338     */
339    public static void setNextAlert(final Context context) {
340        if (!enableSnoozeAlert(context)) {
341            Alarm alarm = calculateNextAlert(context);
342            if (alarm != null) {
343                enableAlert(context, alarm, alarm.time);
344            } else {
345                disableAlert(context);
346            }
347        }
348    }
349
350    /**
351     * Sets alert in AlarmManger and StatusBar.  This is what will
352     * actually launch the alert when the alarm triggers.
353     *
354     * @param alarm Alarm.
355     * @param atTimeInMillis milliseconds since epoch
356     */
357    private static void enableAlert(Context context, final Alarm alarm,
358            final long atTimeInMillis) {
359        AlarmManager am = (AlarmManager)
360                context.getSystemService(Context.ALARM_SERVICE);
361
362        if (Log.LOGV) {
363            Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis);
364        }
365
366        Intent intent = new Intent(ALARM_ALERT_ACTION);
367
368        // XXX: This is a slight hack to avoid an exception in the remote
369        // AlarmManagerService process. The AlarmManager adds extra data to
370        // this Intent which causes it to inflate. Since the remote process
371        // does not know about the Alarm class, it throws a
372        // ClassNotFoundException.
373        //
374        // To avoid this, we marshall the data ourselves and then parcel a plain
375        // byte[] array. The AlarmReceiver class knows to build the Alarm
376        // object from the byte[] array.
377        Parcel out = Parcel.obtain();
378        alarm.writeToParcel(out, 0);
379        out.setDataPosition(0);
380        intent.putExtra(ALARM_RAW_DATA, out.marshall());
381
382        PendingIntent sender = PendingIntent.getBroadcast(
383                context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
384
385        am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
386
387        setStatusBarIcon(context, true);
388
389        Calendar c = Calendar.getInstance();
390        c.setTimeInMillis(atTimeInMillis);
391        String timeString = formatDayAndTime(context, c);
392        saveNextAlarm(context, timeString);
393    }
394
395    /**
396     * Disables alert in AlarmManger and StatusBar.
397     *
398     * @param id Alarm ID.
399     */
400    static void disableAlert(Context context) {
401        AlarmManager am = (AlarmManager)
402                context.getSystemService(Context.ALARM_SERVICE);
403        PendingIntent sender = PendingIntent.getBroadcast(
404                context, 0, new Intent(ALARM_ALERT_ACTION),
405                PendingIntent.FLAG_CANCEL_CURRENT);
406        am.cancel(sender);
407        setStatusBarIcon(context, false);
408        saveNextAlarm(context, "");
409    }
410
411    static void saveSnoozeAlert(final Context context, final int id,
412            final long time) {
413        SharedPreferences prefs = context.getSharedPreferences(
414                AlarmClock.PREFERENCES, 0);
415        if (id == -1) {
416            clearSnoozePreference(context, prefs);
417        } else {
418            SharedPreferences.Editor ed = prefs.edit();
419            ed.putInt(PREF_SNOOZE_ID, id);
420            ed.putLong(PREF_SNOOZE_TIME, time);
421            ed.commit();
422        }
423        // Set the next alert after updating the snooze.
424        setNextAlert(context);
425    }
426
427    /**
428     * Disable the snooze alert if the given id matches the snooze id.
429     */
430    static void disableSnoozeAlert(final Context context, final int id) {
431        SharedPreferences prefs = context.getSharedPreferences(
432                AlarmClock.PREFERENCES, 0);
433        int snoozeId = prefs.getInt(PREF_SNOOZE_ID, -1);
434        if (snoozeId == -1) {
435            // No snooze set, do nothing.
436            return;
437        } else if (snoozeId == id) {
438            // This is the same id so clear the shared prefs.
439            clearSnoozePreference(context, prefs);
440        }
441    }
442
443    // Helper to remove the snooze preference. Do not use clear because that
444    // will erase the clock preferences. Also clear the snooze notification in
445    // the window shade.
446    private static void clearSnoozePreference(final Context context,
447            final SharedPreferences prefs) {
448        final int alarmId = prefs.getInt(PREF_SNOOZE_ID, -1);
449        if (alarmId != -1) {
450            NotificationManager nm = (NotificationManager)
451                    context.getSystemService(Context.NOTIFICATION_SERVICE);
452            nm.cancel(alarmId);
453        }
454
455        final SharedPreferences.Editor ed = prefs.edit();
456        ed.remove(PREF_SNOOZE_ID);
457        ed.remove(PREF_SNOOZE_TIME);
458        ed.commit();
459    };
460
461    /**
462     * If there is a snooze set, enable it in AlarmManager
463     * @return true if snooze is set
464     */
465    private static boolean enableSnoozeAlert(final Context context) {
466        SharedPreferences prefs = context.getSharedPreferences(
467                AlarmClock.PREFERENCES, 0);
468
469        int id = prefs.getInt(PREF_SNOOZE_ID, -1);
470        if (id == -1) {
471            return false;
472        }
473        long time = prefs.getLong(PREF_SNOOZE_TIME, -1);
474
475        // Get the alarm from the db.
476        final Alarm alarm = getAlarm(context.getContentResolver(), id);
477        if (alarm == null) {
478            return false;
479        }
480        // The time in the database is either 0 (repeating) or a specific time
481        // for a non-repeating alarm. Update this value so the AlarmReceiver
482        // has the right time to compare.
483        alarm.time = time;
484
485        enableAlert(context, alarm, time);
486        return true;
487    }
488
489    /**
490     * Tells the StatusBar whether the alarm is enabled or disabled
491     */
492    private static void setStatusBarIcon(Context context, boolean enabled) {
493        Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
494        alarmChanged.putExtra("alarmSet", enabled);
495        context.sendBroadcast(alarmChanged);
496    }
497
498    private static long calculateAlarm(Alarm alarm) {
499        return calculateAlarm(alarm.hour, alarm.minutes, alarm.daysOfWeek)
500                .getTimeInMillis();
501    }
502
503    /**
504     * Given an alarm in hours and minutes, return a time suitable for
505     * setting in AlarmManager.
506     */
507    static Calendar calculateAlarm(int hour, int minute,
508            Alarm.DaysOfWeek daysOfWeek) {
509
510        // start with now
511        Calendar c = Calendar.getInstance();
512        c.setTimeInMillis(System.currentTimeMillis());
513
514        int nowHour = c.get(Calendar.HOUR_OF_DAY);
515        int nowMinute = c.get(Calendar.MINUTE);
516
517        // if alarm is behind current time, advance one day
518        if (hour < nowHour  ||
519            hour == nowHour && minute <= nowMinute) {
520            c.add(Calendar.DAY_OF_YEAR, 1);
521        }
522        c.set(Calendar.HOUR_OF_DAY, hour);
523        c.set(Calendar.MINUTE, minute);
524        c.set(Calendar.SECOND, 0);
525        c.set(Calendar.MILLISECOND, 0);
526
527        int addDays = daysOfWeek.getNextAlarm(c);
528        if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
529        return c;
530    }
531
532    static String formatTime(final Context context, int hour, int minute,
533                             Alarm.DaysOfWeek daysOfWeek) {
534        Calendar c = calculateAlarm(hour, minute, daysOfWeek);
535        return formatTime(context, c);
536    }
537
538    /* used by AlarmAlert */
539    static String formatTime(final Context context, Calendar c) {
540        String format = get24HourMode(context) ? M24 : M12;
541        return (c == null) ? "" : (String)DateFormat.format(format, c);
542    }
543
544    /**
545     * Shows day and time -- used for lock screen
546     */
547    private static String formatDayAndTime(final Context context, Calendar c) {
548        String format = get24HourMode(context) ? DM24 : DM12;
549        return (c == null) ? "" : (String)DateFormat.format(format, c);
550    }
551
552    /**
553     * Save time of the next alarm, as a formatted string, into the system
554     * settings so those who care can make use of it.
555     */
556    static void saveNextAlarm(final Context context, String timeString) {
557        Settings.System.putString(context.getContentResolver(),
558                                  Settings.System.NEXT_ALARM_FORMATTED,
559                                  timeString);
560    }
561
562    /**
563     * @return true if clock is set to 24-hour mode
564     */
565    static boolean get24HourMode(final Context context) {
566        return android.text.format.DateFormat.is24HourFormat(context);
567    }
568}
569