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