OSDN Git Service

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