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