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