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