Alarms.java revision 232725e8c1b1b85a44857cedcc842dcd2c956dc2
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 // 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} 556