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