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