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.alarmclock;
19 import android.app.AlarmManager;
20 import android.app.PendingIntent;
21 import android.content.ContentResolver;
22 import android.content.ContentValues;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.SharedPreferences;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.os.Parcel;
30 import android.provider.Settings;
31 import android.text.format.DateFormat;
33 import java.util.Calendar;
34 import java.text.DateFormatSymbols;
37 * The Alarms provider supplies info about Alarm Clock settings
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.alarmclock.ALARM_ALERT";
46 // This is a private action used when the user clears all notifications.
47 public static final String CLEAR_NOTIFICATION = "clear_notification";
49 // This is a private action used by the AlarmKlaxon to update the UI to
50 // show the alarm has been killed.
51 public static final String ALARM_KILLED = "alarm_killed";
53 // Extra in the ALARM_KILLED intent to indicate to the user how long the
54 // alarm played before being killed.
55 public static final String ALARM_KILLED_TIMEOUT = "alarm_killed_timeout";
57 // This string is used to indicate a silent alarm in the db.
58 public static final String ALARM_ALERT_SILENT = "silent";
60 // This intent is sent from the notification when the user cancels the
62 public static final String CANCEL_SNOOZE = "cancel_snooze";
64 // This string is used when passing an Alarm object through an intent.
65 public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm";
67 // This extra is the raw Alarm object data. It is used in the
68 // AlarmManagerService to avoid a ClassNotFoundException when filling in
70 public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw";
72 // This string is used to identify the alarm id passed to SetAlarm from the
74 public static final String ALARM_ID = "alarm_id";
76 final static String PREF_SNOOZE_ID = "snooze_id";
77 final static String PREF_SNOOZE_TIME = "snooze_time";
79 private final static String DM12 = "E h:mm aa";
80 private final static String DM24 = "E k:mm";
82 private final static String M12 = "h:mm aa";
83 // Shared with DigitalClock
84 final static String M24 = "kk:mm";
87 * Creates a new Alarm.
89 public static Uri addAlarm(ContentResolver contentResolver) {
90 ContentValues values = new ContentValues();
91 values.put(Alarm.Columns.HOUR, 8);
92 return contentResolver.insert(Alarm.Columns.CONTENT_URI, values);
96 * Removes an existing Alarm. If this alarm is snoozing, disables
97 * snooze. Sets next alert.
99 public static void deleteAlarm(
100 Context context, int alarmId) {
102 ContentResolver contentResolver = context.getContentResolver();
103 /* If alarm is snoozing, lose it */
104 disableSnoozeAlert(context, alarmId);
106 Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
107 contentResolver.delete(uri, "", null);
109 setNextAlert(context);
114 * @return cursor over all alarms
116 public static Cursor getAlarmsCursor(ContentResolver contentResolver) {
117 return contentResolver.query(
118 Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS,
119 null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
122 // Private method to get a more limited set of alarms from the database.
123 private static Cursor getFilteredAlarmsCursor(
124 ContentResolver contentResolver) {
125 return contentResolver.query(Alarm.Columns.CONTENT_URI,
126 Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED,
131 * Return an Alarm object representing the alarm id in the database.
132 * Returns null if no alarm exists.
134 public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) {
135 Cursor cursor = contentResolver.query(
136 ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId),
137 Alarm.Columns.ALARM_QUERY_COLUMNS,
140 if (cursor != null) {
141 if (cursor.moveToFirst()) {
142 alarm = new Alarm(cursor);
151 * A convenience method to set an alarm in the Alarms
154 * @param id corresponds to the _id column
155 * @param enabled corresponds to the ENABLED column
156 * @param hour corresponds to the HOUR column
157 * @param minutes corresponds to the MINUTES column
158 * @param daysOfWeek corresponds to the DAYS_OF_WEEK column
159 * @param time corresponds to the ALARM_TIME column
160 * @param vibrate corresponds to the VIBRATE column
161 * @param message corresponds to the MESSAGE column
162 * @param alert corresponds to the ALERT column
164 public static void setAlarm(
165 Context context, int id, boolean enabled, int hour, int minutes,
166 Alarm.DaysOfWeek daysOfWeek, boolean vibrate, String message,
169 ContentValues values = new ContentValues(8);
170 ContentResolver resolver = context.getContentResolver();
171 // Set the alarm_time value if this alarm does not repeat. This will be
172 // used later to disable expired alarms.
174 if (!daysOfWeek.isRepeatSet()) {
175 time = calculateAlarm(hour, minutes, daysOfWeek).getTimeInMillis();
179 "** setAlarm * idx " + id + " hour " + hour + " minutes " +
180 minutes + " enabled " + enabled + " time " + time);
182 values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
183 values.put(Alarm.Columns.HOUR, hour);
184 values.put(Alarm.Columns.MINUTES, minutes);
185 values.put(Alarm.Columns.ALARM_TIME, time);
186 values.put(Alarm.Columns.DAYS_OF_WEEK, daysOfWeek.getCoded());
187 values.put(Alarm.Columns.VIBRATE, vibrate);
188 values.put(Alarm.Columns.MESSAGE, message);
189 values.put(Alarm.Columns.ALERT, alert);
190 resolver.update(ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, id),
193 setNextAlert(context);
197 * A convenience method to enable or disable an alarm.
199 * @param id corresponds to the _id column
200 * @param enabled corresponds to the ENABLED column
203 public static void enableAlarm(
204 final Context context, final int id, boolean enabled) {
205 enableAlarmInternal(context, id, enabled);
206 setNextAlert(context);
209 private static void enableAlarmInternal(final Context context,
210 final int id, boolean enabled) {
211 enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
215 private static void enableAlarmInternal(final Context context,
216 final Alarm alarm, boolean enabled) {
217 ContentResolver resolver = context.getContentResolver();
219 ContentValues values = new ContentValues(2);
220 values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
222 // If we are enabling the alarm, calculate alarm time since the time
223 // value in Alarm may be old.
226 if (!alarm.daysOfWeek.isRepeatSet()) {
227 time = calculateAlarm(alarm.hour, alarm.minutes,
228 alarm.daysOfWeek).getTimeInMillis();
230 values.put(Alarm.Columns.ALARM_TIME, time);
233 resolver.update(ContentUris.withAppendedId(
234 Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
237 public static Alarm calculateNextAlert(final Context context) {
239 long minTime = Long.MAX_VALUE;
240 long now = System.currentTimeMillis();
241 Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver());
242 if (cursor != null) {
243 if (cursor.moveToFirst()) {
245 Alarm a = new Alarm(cursor);
246 // A time of 0 indicates this is a repeating alarm, so
247 // calculate the time to get the next alert.
249 a.time = calculateAlarm(a.hour, a.minutes, a.daysOfWeek)
251 } else if (a.time < now) {
252 // Expired alarm, disable it and move along.
253 enableAlarmInternal(context, a, false);
256 if (a.time < minTime) {
260 } while (cursor.moveToNext());
268 * Disables non-repeating alarms that have passed. Called at
271 public static void disableExpiredAlarms(final Context context) {
272 Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
273 long now = System.currentTimeMillis();
275 if (cur.moveToFirst()) {
277 Alarm alarm = new Alarm(cur);
278 // A time of 0 means this alarm repeats. If the time is
279 // non-zero, check if the time is before now.
280 if (alarm.time != 0 && alarm.time < now) {
282 Log.v("** DISABLE " + alarm.id + " now " + now +" set "
285 enableAlarmInternal(context, alarm, false);
287 } while (cur.moveToNext());
293 * Called at system startup, on time/timezone change, and whenever
294 * the user changes alarm settings. Activates snooze if set,
295 * otherwise loads all alarms, activates next alert.
297 public static void setNextAlert(final Context context) {
298 if (!enableSnoozeAlert(context)) {
299 Alarm alarm = calculateNextAlert(context);
301 enableAlert(context, alarm, alarm.time);
303 disableAlert(context);
309 * Sets alert in AlarmManger and StatusBar. This is what will
310 * actually launch the alert when the alarm triggers.
312 * @param alarm Alarm.
313 * @param atTimeInMillis milliseconds since epoch
315 private static void enableAlert(Context context, final Alarm alarm,
316 final long atTimeInMillis) {
317 AlarmManager am = (AlarmManager)
318 context.getSystemService(Context.ALARM_SERVICE);
321 Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis);
324 Intent intent = new Intent(ALARM_ALERT_ACTION);
326 // XXX: This is a slight hack to avoid an exception in the remote
327 // AlarmManagerService process. The AlarmManager adds extra data to
328 // this Intent which causes it to inflate. Since the remote process
329 // does not know about the Alarm class, it throws a
330 // ClassNotFoundException.
332 // To avoid this, we marshall the data ourselves and then parcel a plain
333 // byte[] array. The AlarmReceiver class knows to build the Alarm
334 // object from the byte[] array.
335 Parcel out = Parcel.obtain();
336 alarm.writeToParcel(out, 0);
337 out.setDataPosition(0);
338 intent.putExtra(ALARM_RAW_DATA, out.marshall());
340 PendingIntent sender = PendingIntent.getBroadcast(
341 context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
343 am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
345 setStatusBarIcon(context, true);
347 Calendar c = Calendar.getInstance();
348 c.setTime(new java.util.Date(atTimeInMillis));
349 String timeString = formatDayAndTime(context, c);
350 saveNextAlarm(context, timeString);
354 * Disables alert in AlarmManger and StatusBar.
356 * @param id Alarm ID.
358 static void disableAlert(Context context) {
359 AlarmManager am = (AlarmManager)
360 context.getSystemService(Context.ALARM_SERVICE);
361 PendingIntent sender = PendingIntent.getBroadcast(
362 context, 0, new Intent(ALARM_ALERT_ACTION),
363 PendingIntent.FLAG_CANCEL_CURRENT);
365 setStatusBarIcon(context, false);
366 saveNextAlarm(context, "");
369 static void saveSnoozeAlert(final Context context, final int id,
371 SharedPreferences prefs = context.getSharedPreferences(
372 AlarmClock.PREFERENCES, 0);
373 SharedPreferences.Editor ed = prefs.edit();
375 clearSnoozePreference(ed);
377 ed.putInt(PREF_SNOOZE_ID, id);
378 ed.putLong(PREF_SNOOZE_TIME, time);
381 // Set the next alert after updating the snooze.
382 setNextAlert(context);
386 * Disable the snooze alert if the given id matches the snooze id.
388 static void disableSnoozeAlert(final Context context, final int id) {
389 SharedPreferences prefs = context.getSharedPreferences(
390 AlarmClock.PREFERENCES, 0);
391 int snoozeId = prefs.getInt(PREF_SNOOZE_ID, -1);
392 if (snoozeId == -1) {
393 // No snooze set, do nothing.
395 } else if (snoozeId == id) {
396 // This is the same id so clear the shared prefs.
397 clearSnoozePreference(prefs.edit());
401 // Helper to remove the snooze preference. Do not use clear because that
402 // will erase the clock preferences.
403 private static void clearSnoozePreference(final SharedPreferences.Editor ed) {
404 ed.remove(PREF_SNOOZE_ID);
405 ed.remove(PREF_SNOOZE_TIME);
410 * If there is a snooze set, enable it in AlarmManager
411 * @return true if snooze is set
413 private static boolean enableSnoozeAlert(final Context context) {
414 SharedPreferences prefs = context.getSharedPreferences(
415 AlarmClock.PREFERENCES, 0);
417 int id = prefs.getInt(PREF_SNOOZE_ID, -1);
421 long time = prefs.getLong(PREF_SNOOZE_TIME, -1);
423 // Get the alarm from the db.
424 final Alarm alarm = getAlarm(context.getContentResolver(), id);
425 // The time in the database is either 0 (repeating) or a specific time
426 // for a non-repeating alarm. Update this value so the AlarmReceiver
427 // has the right time to compare.
430 enableAlert(context, alarm, time);
435 * Tells the StatusBar whether the alarm is enabled or disabled
437 private static void setStatusBarIcon(Context context, boolean enabled) {
438 Intent alarmChanged = new Intent(Intent.ACTION_ALARM_CHANGED);
439 alarmChanged.putExtra("alarmSet", enabled);
440 context.sendBroadcast(alarmChanged);
444 * Given an alarm in hours and minutes, return a time suitable for
445 * setting in AlarmManager.
446 * @param hour Always in 24 hour 0-23
448 * @param daysOfWeek 0-59
450 static Calendar calculateAlarm(int hour, int minute, Alarm.DaysOfWeek daysOfWeek) {
453 Calendar c = Calendar.getInstance();
454 c.setTimeInMillis(System.currentTimeMillis());
456 int nowHour = c.get(Calendar.HOUR_OF_DAY);
457 int nowMinute = c.get(Calendar.MINUTE);
459 // if alarm is behind current time, advance one day
460 if (hour < nowHour ||
461 hour == nowHour && minute <= nowMinute) {
462 c.add(Calendar.DAY_OF_YEAR, 1);
464 c.set(Calendar.HOUR_OF_DAY, hour);
465 c.set(Calendar.MINUTE, minute);
466 c.set(Calendar.SECOND, 0);
467 c.set(Calendar.MILLISECOND, 0);
469 int addDays = daysOfWeek.getNextAlarm(c);
470 /* Log.v("** TIMES * " + c.getTimeInMillis() + " hour " + hour +
471 " minute " + minute + " dow " + c.get(Calendar.DAY_OF_WEEK) + " from now " +
473 if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
477 static String formatTime(final Context context, int hour, int minute,
478 Alarm.DaysOfWeek daysOfWeek) {
479 Calendar c = calculateAlarm(hour, minute, daysOfWeek);
480 return formatTime(context, c);
483 /* used by AlarmAlert */
484 static String formatTime(final Context context, Calendar c) {
485 String format = get24HourMode(context) ? M24 : M12;
486 return (c == null) ? "" : (String)DateFormat.format(format, c);
490 * Shows day and time -- used for lock screen
492 private static String formatDayAndTime(final Context context, Calendar c) {
493 String format = get24HourMode(context) ? DM24 : DM12;
494 return (c == null) ? "" : (String)DateFormat.format(format, c);
498 * Save time of the next alarm, as a formatted string, into the system
499 * settings so those who care can make use of it.
501 static void saveNextAlarm(final Context context, String timeString) {
502 Settings.System.putString(context.getContentResolver(),
503 Settings.System.NEXT_ALARM_FORMATTED,
508 * @return true if clock is set to 24-hour mode
510 static boolean get24HourMode(final Context context) {
511 return android.text.format.DateFormat.is24HourFormat(context);