OSDN Git Service

69047a0742afac8e38dd880e0157dc5c5646003d
[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 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 }