2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.deskclock;
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;
34 import java.util.Calendar;
35 import java.text.DateFormatSymbols;
38 * The Alarms provider supplies info about Alarm Clock settings
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";
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";
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";
55 // This string is used to indicate a silent alarm in the db.
56 public static final String ALARM_ALERT_SILENT = "silent";
58 // This intent is sent from the notification when the user cancels the
60 public static final String CANCEL_SNOOZE = "cancel_snooze";
62 // This string is used when passing an Alarm object through an intent.
63 public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm";
65 // This extra is the raw Alarm object data. It is used in the
66 // AlarmManagerService to avoid a ClassNotFoundException when filling in
68 public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw";
70 // This string is used to identify the alarm id passed to SetAlarm from the
72 public static final String ALARM_ID = "alarm_id";
74 final static String PREF_SNOOZE_ID = "snooze_id";
75 final static String PREF_SNOOZE_TIME = "snooze_time";
77 private final static String DM12 = "E h:mm aa";
78 private final static String DM24 = "E k:mm";
80 private final static String M12 = "h:mm aa";
81 // Shared with DigitalClock
82 final static String M24 = "kk:mm";
85 * Creates a new Alarm.
87 public static long addAlarm(Context context, Alarm alarm) {
88 ContentValues values = createContentValues(alarm);
89 context.getContentResolver().insert(Alarm.Columns.CONTENT_URI, values);
91 long timeInMillis = calculateAlarm(alarm);
93 clearSnoozeIfNeeded(context, timeInMillis);
95 setNextAlert(context);
100 * Removes an existing Alarm. If this alarm is snoozing, disables
101 * snooze. Sets next alert.
103 public static void deleteAlarm(
104 Context context, int alarmId) {
106 ContentResolver contentResolver = context.getContentResolver();
107 /* If alarm is snoozing, lose it */
108 disableSnoozeAlert(context, alarmId);
110 Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
111 contentResolver.delete(uri, "", null);
113 setNextAlert(context);
118 * @return cursor over all alarms
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);
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,
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.
139 if (!alarm.daysOfWeek.isRepeatSet()) {
140 time = calculateAlarm(alarm);
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);
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());
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);
170 * Return an Alarm object representing the alarm id in the database.
171 * Returns null if no alarm exists.
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,
179 if (cursor != null) {
180 if (cursor.moveToFirst()) {
181 alarm = new Alarm(cursor);
190 * A convenience method to set an alarm in the Alarms
192 * @return Time when the alarm will fire.
194 public static long setAlarm(Context context, Alarm alarm) {
195 ContentValues values = createContentValues(alarm);
196 ContentResolver resolver = context.getContentResolver();
198 ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarm.id),
201 long timeInMillis = calculateAlarm(alarm);
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
207 // TODO: disableSnoozeAlert should have a better name.
208 disableSnoozeAlert(context, alarm.id);
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);
216 setNextAlert(context);
222 * A convenience method to enable or disable an alarm.
224 * @param id corresponds to the _id column
225 * @param enabled corresponds to the ENABLED column
228 public static void enableAlarm(
229 final Context context, final int id, boolean enabled) {
230 enableAlarmInternal(context, id, enabled);
231 setNextAlert(context);
234 private static void enableAlarmInternal(final Context context,
235 final int id, boolean enabled) {
236 enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
240 private static void enableAlarmInternal(final Context context,
241 final Alarm alarm, boolean enabled) {
245 ContentResolver resolver = context.getContentResolver();
247 ContentValues values = new ContentValues(2);
248 values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
250 // If we are enabling the alarm, calculate alarm time since the time
251 // value in Alarm may be old.
254 if (!alarm.daysOfWeek.isRepeatSet()) {
255 time = calculateAlarm(alarm);
257 values.put(Alarm.Columns.ALARM_TIME, time);
259 // Clear the snooze if the id matches.
260 disableSnoozeAlert(context, alarm.id);
263 resolver.update(ContentUris.withAppendedId(
264 Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
267 public static Alarm calculateNextAlert(final Context context) {
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()) {
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.
279 a.time = calculateAlarm(a);
280 } else if (a.time < now) {
281 // Expired alarm, disable it and move along.
282 enableAlarmInternal(context, a, false);
285 if (a.time < minTime) {
289 } while (cursor.moveToNext());
297 * Disables non-repeating alarms that have passed. Called at
300 public static void disableExpiredAlarms(final Context context) {
301 Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
302 long now = System.currentTimeMillis();
304 if (cur.moveToFirst()) {
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) {
311 Log.v("** DISABLE " + alarm.id + " now " + now +" set "
314 enableAlarmInternal(context, alarm, false);
316 } while (cur.moveToNext());
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.
326 public static void setNextAlert(final Context context) {
327 if (!enableSnoozeAlert(context)) {
328 Alarm alarm = calculateNextAlert(context);
330 enableAlert(context, alarm, alarm.time);
332 disableAlert(context);
338 * Sets alert in AlarmManger and StatusBar. This is what will
339 * actually launch the alert when the alarm triggers.
341 * @param alarm Alarm.
342 * @param atTimeInMillis milliseconds since epoch
344 private static void enableAlert(Context context, final Alarm alarm,
345 final long atTimeInMillis) {
346 AlarmManager am = (AlarmManager)
347 context.getSystemService(Context.ALARM_SERVICE);
350 Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis);
353 Intent intent = new Intent(ALARM_ALERT_ACTION);
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.
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());
369 PendingIntent sender = PendingIntent.getBroadcast(
370 context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
372 am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
374 setStatusBarIcon(context, true);
376 Calendar c = Calendar.getInstance();
377 c.setTimeInMillis(atTimeInMillis);
378 String timeString = formatDayAndTime(context, c);
379 saveNextAlarm(context, timeString);
383 * Disables alert in AlarmManger and StatusBar.
385 * @param id Alarm ID.
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);
394 setStatusBarIcon(context, false);
395 saveNextAlarm(context, "");
398 static void saveSnoozeAlert(final Context context, final int id,
400 SharedPreferences prefs = context.getSharedPreferences(
401 AlarmClock.PREFERENCES, 0);
403 clearSnoozePreference(context, prefs);
405 SharedPreferences.Editor ed = prefs.edit();
406 ed.putInt(PREF_SNOOZE_ID, id);
407 ed.putLong(PREF_SNOOZE_TIME, time);
410 // Set the next alert after updating the snooze.
411 setNextAlert(context);
415 * Disable the snooze alert if the given id matches the snooze id.
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.
424 } else if (snoozeId == id) {
425 // This is the same id so clear the shared prefs.
426 clearSnoozePreference(context, prefs);
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
433 private static void clearSnoozePreference(final Context context,
434 final SharedPreferences prefs) {
435 final int alarmId = prefs.getInt(PREF_SNOOZE_ID, -1);
437 NotificationManager nm = (NotificationManager)
438 context.getSystemService(Context.NOTIFICATION_SERVICE);
442 final SharedPreferences.Editor ed = prefs.edit();
443 ed.remove(PREF_SNOOZE_ID);
444 ed.remove(PREF_SNOOZE_TIME);
449 * If there is a snooze set, enable it in AlarmManager
450 * @return true if snooze is set
452 private static boolean enableSnoozeAlert(final Context context) {
453 SharedPreferences prefs = context.getSharedPreferences(
454 AlarmClock.PREFERENCES, 0);
456 int id = prefs.getInt(PREF_SNOOZE_ID, -1);
460 long time = prefs.getLong(PREF_SNOOZE_TIME, -1);
462 // Get the alarm from the db.
463 final Alarm alarm = getAlarm(context.getContentResolver(), id);
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.
472 enableAlert(context, alarm, time);
477 * Tells the StatusBar whether the alarm is enabled or disabled
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);
485 private static long calculateAlarm(Alarm alarm) {
486 return calculateAlarm(alarm.hour, alarm.minutes, alarm.daysOfWeek)
491 * Given an alarm in hours and minutes, return a time suitable for
492 * setting in AlarmManager.
494 static Calendar calculateAlarm(int hour, int minute,
495 Alarm.DaysOfWeek daysOfWeek) {
498 Calendar c = Calendar.getInstance();
499 c.setTimeInMillis(System.currentTimeMillis());
501 int nowHour = c.get(Calendar.HOUR_OF_DAY);
502 int nowMinute = c.get(Calendar.MINUTE);
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);
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);
514 int addDays = daysOfWeek.getNextAlarm(c);
515 if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
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);
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);
532 * Shows day and time -- used for lock screen
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);
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.
543 static void saveNextAlarm(final Context context, String timeString) {
544 Settings.System.putString(context.getContentResolver(),
545 Settings.System.NEXT_ALARM_FORMATTED,
550 * @return true if clock is set to 24-hour mode
552 static boolean get24HourMode(final Context context) {
553 return android.text.format.DateFormat.is24HourFormat(context);