Alarms.java revision c6098ef1fc69fe117343cc2e3db0d2b97b2daa6f
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 and fills in the given alarm's id.
99     */
100    public static long addAlarm(Context context, Alarm alarm) {
101        ContentValues values = createContentValues(alarm);
102        Uri uri = context.getContentResolver().insert(
103                Alarm.Columns.CONTENT_URI, values);
104        alarm.id = (int) ContentUris.parseId(uri);
105
106        long timeInMillis = calculateAlarm(alarm);
107        if (alarm.enabled) {
108            clearSnoozeIfNeeded(context, timeInMillis);
109        }
110        setNextAlert(context);
111        return timeInMillis;
112    }
113
114    /**
115     * Removes an existing Alarm.  If this alarm is snoozing, disables
116     * snooze.  Sets next alert.
117     */
118    public static void deleteAlarm(Context context, int alarmId) {
119        if (alarmId == -1) return;
120
121        ContentResolver contentResolver = context.getContentResolver();
122        /* If alarm is snoozing, lose it */
123        disableSnoozeAlert(context, alarmId);
124
125        Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
126        contentResolver.delete(uri, "", null);
127
128        setNextAlert(context);
129    }
130
131    /**
132     * Queries all alarms
133     * @return cursor over all alarms
134     */
135    public static Cursor getAlarmsCursor(ContentResolver contentResolver) {
136        return contentResolver.query(
137                Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS,
138                null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
139    }
140
141    // Private method to get a more limited set of alarms from the database.
142    private static Cursor getFilteredAlarmsCursor(
143            ContentResolver contentResolver) {
144        return contentResolver.query(Alarm.Columns.CONTENT_URI,
145                Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED,
146                null, null);
147    }
148
149    private static ContentValues createContentValues(Alarm alarm) {
150        ContentValues values = new ContentValues(8);
151        // Set the alarm_time value if this alarm does not repeat. This will be
152        // used later to disable expire alarms.
153        long time = 0;
154        if (!alarm.daysOfWeek.isRepeatSet()) {
155            time = calculateAlarm(alarm);
156        }
157
158        values.put(Alarm.Columns.ENABLED, alarm.enabled ? 1 : 0);
159        values.put(Alarm.Columns.HOUR, alarm.hour);
160        values.put(Alarm.Columns.MINUTES, alarm.minutes);
161        values.put(Alarm.Columns.ALARM_TIME, alarm.time);
162        values.put(Alarm.Columns.DAYS_OF_WEEK, alarm.daysOfWeek.getCoded());
163        values.put(Alarm.Columns.VIBRATE, alarm.vibrate);
164        values.put(Alarm.Columns.MESSAGE, alarm.label);
165
166        // A null alert Uri indicates a silent alarm.
167        values.put(Alarm.Columns.ALERT, alarm.alert == null ? ALARM_ALERT_SILENT
168                : alarm.alert.toString());
169
170        return values;
171    }
172
173    private static void clearSnoozeIfNeeded(Context context, long alarmTime) {
174        // If this alarm fires before the next snooze, clear the snooze to
175        // enable this alarm.
176        SharedPreferences prefs =
177                context.getSharedPreferences(AlarmClock.PREFERENCES, 0);
178        long snoozeTime = prefs.getLong(PREF_SNOOZE_TIME, 0);
179        if (alarmTime < snoozeTime) {
180            clearSnoozePreference(context, prefs);
181        }
182    }
183
184    /**
185     * Return an Alarm object representing the alarm id in the database.
186     * Returns null if no alarm exists.
187     */
188    public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) {
189        Cursor cursor = contentResolver.query(
190                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId),
191                Alarm.Columns.ALARM_QUERY_COLUMNS,
192                null, null, null);
193        Alarm alarm = null;
194        if (cursor != null) {
195            if (cursor.moveToFirst()) {
196                alarm = new Alarm(cursor);
197            }
198            cursor.close();
199        }
200        return alarm;
201    }
202
203
204    /**
205     * A convenience method to set an alarm in the Alarms
206     * content provider.
207     * @return Time when the alarm will fire.
208     */
209    public static long setAlarm(Context context, Alarm alarm) {
210        ContentValues values = createContentValues(alarm);
211        ContentResolver resolver = context.getContentResolver();
212        resolver.update(
213                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarm.id),
214                values, null, null);
215
216        long timeInMillis = calculateAlarm(alarm);
217
218        if (alarm.enabled) {
219            // Disable the snooze if we just changed the snoozed alarm. This
220            // only does work if the snoozed alarm is the same as the given
221            // alarm.
222            // TODO: disableSnoozeAlert should have a better name.
223            disableSnoozeAlert(context, alarm.id);
224
225            // Disable the snooze if this alarm fires before the snoozed alarm.
226            // This works on every alarm since the user most likely intends to
227            // have the modified alarm fire next.
228            clearSnoozeIfNeeded(context, timeInMillis);
229        }
230
231        setNextAlert(context);
232
233        return timeInMillis;
234    }
235
236    /**
237     * A convenience method to enable or disable an alarm.
238     *
239     * @param id             corresponds to the _id column
240     * @param enabled        corresponds to the ENABLED column
241     */
242
243    public static void enableAlarm(
244            final Context context, final int id, boolean enabled) {
245        enableAlarmInternal(context, id, enabled);
246        setNextAlert(context);
247    }
248
249    private static void enableAlarmInternal(final Context context,
250            final int id, boolean enabled) {
251        enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
252                enabled);
253    }
254
255    private static void enableAlarmInternal(final Context context,
256            final Alarm alarm, boolean enabled) {
257        if (alarm == null) {
258            return;
259        }
260        ContentResolver resolver = context.getContentResolver();
261
262        ContentValues values = new ContentValues(2);
263        values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
264
265        // If we are enabling the alarm, calculate alarm time since the time
266        // value in Alarm may be old.
267        if (enabled) {
268            long time = 0;
269            if (!alarm.daysOfWeek.isRepeatSet()) {
270                time = calculateAlarm(alarm);
271            }
272            values.put(Alarm.Columns.ALARM_TIME, time);
273        } else {
274            // Clear the snooze if the id matches.
275            disableSnoozeAlert(context, alarm.id);
276        }
277
278        resolver.update(ContentUris.withAppendedId(
279                Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
280    }
281
282    public static Alarm calculateNextAlert(final Context context) {
283        Alarm alarm = null;
284        long minTime = Long.MAX_VALUE;
285        long now = System.currentTimeMillis();
286        Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver());
287        if (cursor != null) {
288            if (cursor.moveToFirst()) {
289                do {
290                    Alarm a = new Alarm(cursor);
291                    // A time of 0 indicates this is a repeating alarm, so
292                    // calculate the time to get the next alert.
293                    if (a.time == 0) {
294                        a.time = calculateAlarm(a);
295                    } else if (a.time < now) {
296                        Log.v("Disabling expired alarm set for " +
297                              Log.formatTime(a.time));
298                        // Expired alarm, disable it and move along.
299                        enableAlarmInternal(context, a, false);
300                        continue;
301                    }
302                    if (a.time < minTime) {
303                        minTime = a.time;
304                        alarm = a;
305                    }
306                } while (cursor.moveToNext());
307            }
308            cursor.close();
309        }
310        return alarm;
311    }
312
313    /**
314     * Disables non-repeating alarms that have passed.  Called at
315     * boot.
316     */
317    public static void disableExpiredAlarms(final Context context) {
318        Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
319        long now = System.currentTimeMillis();
320
321        if (cur.moveToFirst()) {
322            do {
323                Alarm alarm = new Alarm(cur);
324                // A time of 0 means this alarm repeats. If the time is
325                // non-zero, check if the time is before now.
326                if (alarm.time != 0 && alarm.time < now) {
327                    Log.v("Disabling expired alarm set for " +
328                          Log.formatTime(alarm.time));
329                    enableAlarmInternal(context, alarm, false);
330                }
331            } while (cur.moveToNext());
332        }
333        cur.close();
334    }
335
336    /**
337     * Called at system startup, on time/timezone change, and whenever
338     * the user changes alarm settings.  Activates snooze if set,
339     * otherwise loads all alarms, activates next alert.
340     */
341    public static void setNextAlert(final Context context) {
342        if (!enableSnoozeAlert(context)) {
343            Alarm alarm = calculateNextAlert(context);
344            if (alarm != null) {
345                enableAlert(context, alarm, alarm.time);
346            } else {
347                disableAlert(context);
348            }
349        }
350    }
351
352    /**
353     * Sets alert in AlarmManger and StatusBar.  This is what will
354     * actually launch the alert when the alarm triggers.
355     *
356     * @param alarm Alarm.
357     * @param atTimeInMillis milliseconds since epoch
358     */
359    private static void enableAlert(Context context, final Alarm alarm,
360            final long atTimeInMillis) {
361        AlarmManager am = (AlarmManager)
362                context.getSystemService(Context.ALARM_SERVICE);
363
364        if (Log.LOGV) {
365            Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis);
366        }
367
368        Intent intent = new Intent(ALARM_ALERT_ACTION);
369
370        // XXX: This is a slight hack to avoid an exception in the remote
371        // AlarmManagerService process. The AlarmManager adds extra data to
372        // this Intent which causes it to inflate. Since the remote process
373        // does not know about the Alarm class, it throws a
374        // ClassNotFoundException.
375        //
376        // To avoid this, we marshall the data ourselves and then parcel a plain
377        // byte[] array. The AlarmReceiver class knows to build the Alarm
378        // object from the byte[] array.
379        Parcel out = Parcel.obtain();
380        alarm.writeToParcel(out, 0);
381        out.setDataPosition(0);
382        intent.putExtra(ALARM_RAW_DATA, out.marshall());
383
384        PendingIntent sender = PendingIntent.getBroadcast(
385                context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
386
387        am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
388
389        setStatusBarIcon(context, true);
390
391        Calendar c = Calendar.getInstance();
392        c.setTimeInMillis(atTimeInMillis);
393        String timeString = formatDayAndTime(context, c);
394        saveNextAlarm(context, timeString);
395    }
396
397    /**
398     * Disables alert in AlarmManger and StatusBar.
399     *
400     * @param id Alarm ID.
401     */
402    static void disableAlert(Context context) {
403        AlarmManager am = (AlarmManager)
404                context.getSystemService(Context.ALARM_SERVICE);
405        PendingIntent sender = PendingIntent.getBroadcast(
406                context, 0, new Intent(ALARM_ALERT_ACTION),
407                PendingIntent.FLAG_CANCEL_CURRENT);
408        am.cancel(sender);
409        setStatusBarIcon(context, false);
410        saveNextAlarm(context, "");
411    }
412
413    static void saveSnoozeAlert(final Context context, final int id,
414            final long time) {
415        SharedPreferences prefs = context.getSharedPreferences(
416                AlarmClock.PREFERENCES, 0);
417        if (id == -1) {
418            clearSnoozePreference(context, prefs);
419        } else {
420            SharedPreferences.Editor ed = prefs.edit();
421            ed.putInt(PREF_SNOOZE_ID, id);
422            ed.putLong(PREF_SNOOZE_TIME, time);
423            ed.apply();
424        }
425        // Set the next alert after updating the snooze.
426        setNextAlert(context);
427    }
428
429    /**
430     * Disable the snooze alert if the given id matches the snooze id.
431     */
432    static void disableSnoozeAlert(final Context context, final int id) {
433        SharedPreferences prefs = context.getSharedPreferences(
434                AlarmClock.PREFERENCES, 0);
435        int snoozeId = prefs.getInt(PREF_SNOOZE_ID, -1);
436        if (snoozeId == -1) {
437            // No snooze set, do nothing.
438            return;
439        } else if (snoozeId == id) {
440            // This is the same id so clear the shared prefs.
441            clearSnoozePreference(context, prefs);
442        }
443    }
444
445    // Helper to remove the snooze preference. Do not use clear because that
446    // will erase the clock preferences. Also clear the snooze notification in
447    // the window shade.
448    private static void clearSnoozePreference(final Context context,
449            final SharedPreferences prefs) {
450        final int alarmId = prefs.getInt(PREF_SNOOZE_ID, -1);
451        if (alarmId != -1) {
452            NotificationManager nm = (NotificationManager)
453                    context.getSystemService(Context.NOTIFICATION_SERVICE);
454            nm.cancel(alarmId);
455        }
456
457        final SharedPreferences.Editor ed = prefs.edit();
458        ed.remove(PREF_SNOOZE_ID);
459        ed.remove(PREF_SNOOZE_TIME);
460        ed.apply();
461    };
462
463    /**
464     * If there is a snooze set, enable it in AlarmManager
465     * @return true if snooze is set
466     */
467    private static boolean enableSnoozeAlert(final Context context) {
468        SharedPreferences prefs = context.getSharedPreferences(
469                AlarmClock.PREFERENCES, 0);
470
471        int id = prefs.getInt(PREF_SNOOZE_ID, -1);
472        if (id == -1) {
473            return false;
474        }
475        long time = prefs.getLong(PREF_SNOOZE_TIME, -1);
476
477        // Get the alarm from the db.
478        final Alarm alarm = getAlarm(context.getContentResolver(), id);
479        if (alarm == null) {
480            return false;
481        }
482        // The time in the database is either 0 (repeating) or a specific time
483        // for a non-repeating alarm. Update this value so the AlarmReceiver
484        // has the right time to compare.
485        alarm.time = time;
486
487        enableAlert(context, alarm, time);
488        return true;
489    }
490
491    /**
492     * Tells the StatusBar whether the alarm is enabled or disabled
493     */
494    private static void setStatusBarIcon(Context context, boolean enabled) {
495        Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
496        alarmChanged.putExtra("alarmSet", enabled);
497        context.sendBroadcast(alarmChanged);
498    }
499
500    private static long calculateAlarm(Alarm alarm) {
501        return calculateAlarm(alarm.hour, alarm.minutes, alarm.daysOfWeek)
502                .getTimeInMillis();
503    }
504
505    /**
506     * Given an alarm in hours and minutes, return a time suitable for
507     * setting in AlarmManager.
508     */
509    static Calendar calculateAlarm(int hour, int minute,
510            Alarm.DaysOfWeek daysOfWeek) {
511
512        // start with now
513        Calendar c = Calendar.getInstance();
514        c.setTimeInMillis(System.currentTimeMillis());
515
516        int nowHour = c.get(Calendar.HOUR_OF_DAY);
517        int nowMinute = c.get(Calendar.MINUTE);
518
519        // if alarm is behind current time, advance one day
520        if (hour < nowHour  ||
521            hour == nowHour && minute <= nowMinute) {
522            c.add(Calendar.DAY_OF_YEAR, 1);
523        }
524        c.set(Calendar.HOUR_OF_DAY, hour);
525        c.set(Calendar.MINUTE, minute);
526        c.set(Calendar.SECOND, 0);
527        c.set(Calendar.MILLISECOND, 0);
528
529        int addDays = daysOfWeek.getNextAlarm(c);
530        if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
531        return c;
532    }
533
534    static String formatTime(final Context context, int hour, int minute,
535                             Alarm.DaysOfWeek daysOfWeek) {
536        Calendar c = calculateAlarm(hour, minute, daysOfWeek);
537        return formatTime(context, c);
538    }
539
540    /* used by AlarmAlert */
541    static String formatTime(final Context context, Calendar c) {
542        String format = get24HourMode(context) ? M24 : M12;
543        return (c == null) ? "" : (String)DateFormat.format(format, c);
544    }
545
546    /**
547     * Shows day and time -- used for lock screen
548     */
549    private static String formatDayAndTime(final Context context, Calendar c) {
550        String format = get24HourMode(context) ? DM24 : DM12;
551        return (c == null) ? "" : (String)DateFormat.format(format, c);
552    }
553
554    /**
555     * Save time of the next alarm, as a formatted string, into the system
556     * settings so those who care can make use of it.
557     */
558    static void saveNextAlarm(final Context context, String timeString) {
559        Settings.System.putString(context.getContentResolver(),
560                                  Settings.System.NEXT_ALARM_FORMATTED,
561                                  timeString);
562    }
563
564    /**
565     * @return true if clock is set to 24-hour mode
566     */
567    static boolean get24HourMode(final Context context) {
568        return android.text.format.DateFormat.is24HourFormat(context);
569    }
570}
571