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