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