blob: 2c38a40c0449dd0482b82b0b5005a90816c3e9cd [file] [log] [blame]
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.deskclock;
import android.app.AlarmManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Parcel;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.DateFormat;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
/**
* The Alarms provider supplies info about Alarm Clock settings
*/
public class Alarms {
static final String PREFERENCES = "AlarmClock";
// This action triggers the AlarmReceiver as well as the AlarmKlaxon. It
// is a public action used in the manifest for receiving Alarm broadcasts
// from the alarm manager.
public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
// A public action sent by AlarmKlaxon when the alarm has stopped sounding
// for any reason (e.g. because it has been dismissed from AlarmAlertFullScreen,
// or killed due to an incoming phone call, etc).
public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
// AlarmAlertFullScreen listens for this broadcast intent, so that other applications
// can snooze the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
// AlarmAlertFullScreen listens for this broadcast intent, so that other applications
// can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
// A public action sent by AlarmAlertFullScreen when a snoozed alarm was dismissed due
// to it handling ALARM_DISMISS_ACTION cancelled
public static final String ALARM_SNOOZE_CANCELLED = "com.android.deskclock.ALARM_SNOOZE_CANCELLED";
// A broadcast sent every time the next alarm time is set in the system
public static final String NEXT_ALARM_TIME_SET = "com.android.deskclock.NEXT_ALARM_TIME_SET";
// This is a private action used by the AlarmKlaxon to update the UI to
// show the alarm has been killed.
public static final String ALARM_KILLED = "alarm_killed";
// Extra in the ALARM_KILLED intent to indicate to the user how long the
// alarm played before being killed.
public static final String ALARM_KILLED_TIMEOUT = "alarm_killed_timeout";
// Extra in the ALARM_KILLED intent to indicate when alarm was replaced
public static final String ALARM_REPLACED = "alarm_replaced";
// This string is used to indicate a silent alarm in the db.
public static final String ALARM_ALERT_SILENT = "silent";
// This intent is sent from the notification when the user cancels the
// snooze alert.
public static final String CANCEL_SNOOZE = "cancel_snooze";
// This string is used when passing an Alarm object through an intent.
public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm";
// This extra is the raw Alarm object data. It is used in the
// AlarmManagerService to avoid a ClassNotFoundException when filling in
// the Intent extras.
public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw";
private static final String PREF_SNOOZE_IDS = "snooze_ids";
private static final String PREF_SNOOZE_TIME = "snooze_time";
private final static String DM12 = "E h:mm aa";
private final static String DM24 = "E kk:mm";
private final static String M12 = "h:mm aa";
// Shared with DigitalClock
final static String M24 = "kk:mm";
final static int INVALID_ALARM_ID = -1;
/**
* Creates a new Alarm and fills in the given alarm's id.
*/
public static long addAlarm(Context context, Alarm alarm) {
ContentValues values = createContentValues(alarm);
Uri uri = context.getContentResolver().insert(
Alarm.Columns.CONTENT_URI, values);
alarm.id = (int) ContentUris.parseId(uri);
long timeInMillis = calculateAlarm(alarm);
if (alarm.enabled) {
clearSnoozeIfNeeded(context, timeInMillis);
}
setNextAlert(context);
return timeInMillis;
}
/**
* Removes an existing Alarm. If this alarm is snoozing, disables
* snooze. Sets next alert.
*/
public static void deleteAlarm(Context context, int alarmId) {
if (alarmId == INVALID_ALARM_ID) return;
ContentResolver contentResolver = context.getContentResolver();
/* If alarm is snoozing, lose it */
disableSnoozeAlert(context, alarmId);
Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
contentResolver.delete(uri, "", null);
setNextAlert(context);
}
public static CursorLoader getAlarmsCursorLoader(Context context) {
return new CursorLoader(context, Alarm.Columns.CONTENT_URI,
Alarm.Columns.ALARM_QUERY_COLUMNS, null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
}
/**
* Queries all alarms
* @return cursor over all alarms
*/
public static Cursor getAlarmsCursor(ContentResolver contentResolver) {
return contentResolver.query(
Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS,
null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
}
// Private method to get a more limited set of alarms from the database.
private static Cursor getFilteredAlarmsCursor(
ContentResolver contentResolver) {
return contentResolver.query(Alarm.Columns.CONTENT_URI,
Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED,
null, null);
}
private static ContentValues createContentValues(Alarm alarm) {
ContentValues values = new ContentValues(8);
// Set the alarm_time value if this alarm does not repeat. This will be
// used later to disable expire alarms.
long time = 0;
if (!alarm.daysOfWeek.isRepeatSet()) {
time = calculateAlarm(alarm);
}
// -1 means generate new id.
if (alarm.id != -1) {
values.put(Alarm.Columns._ID, alarm.id);
}
values.put(Alarm.Columns.ENABLED, alarm.enabled ? 1 : 0);
values.put(Alarm.Columns.HOUR, alarm.hour);
values.put(Alarm.Columns.MINUTES, alarm.minutes);
values.put(Alarm.Columns.ALARM_TIME, time);
values.put(Alarm.Columns.DAYS_OF_WEEK, alarm.daysOfWeek.getCoded());
values.put(Alarm.Columns.VIBRATE, alarm.vibrate);
values.put(Alarm.Columns.MESSAGE, alarm.label);
// A null alert Uri indicates a silent alarm.
values.put(Alarm.Columns.ALERT, alarm.alert == null ? ALARM_ALERT_SILENT
: alarm.alert.toString());
return values;
}
private static void clearSnoozeIfNeeded(Context context, long alarmTime) {
// If this alarm fires before the next snooze, clear the snooze to
// enable this alarm.
SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, 0);
// Get the list of snoozed alarms
final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
for (String snoozedAlarm : snoozedIds) {
final long snoozeTime = prefs.getLong(getAlarmPrefSnoozeTimeKey(snoozedAlarm), 0);
if (alarmTime < snoozeTime) {
final int alarmId = Integer.parseInt(snoozedAlarm);
clearSnoozePreference(context, prefs, alarmId);
}
}
}
/**
* Return an Alarm object representing the alarm id in the database.
* Returns null if no alarm exists.
*/
public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) {
Cursor cursor = contentResolver.query(
ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId),
Alarm.Columns.ALARM_QUERY_COLUMNS,
null, null, null);
Alarm alarm = null;
if (cursor != null) {
if (cursor.moveToFirst()) {
alarm = new Alarm(cursor);
}
cursor.close();
}
return alarm;
}
/**
* A convenience method to set an alarm in the Alarms
* content provider.
* @return Time when the alarm will fire. Or < 1 if update failed.
*/
public static long setAlarm(Context context, Alarm alarm) {
ContentValues values = createContentValues(alarm);
ContentResolver resolver = context.getContentResolver();
long rowsUpdated = resolver.update(
ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarm.id),
values, null, null);
if (rowsUpdated < 1) {
Log.e("Error updating alarm " + alarm);
return rowsUpdated;
}
long timeInMillis = calculateAlarm(alarm);
if (alarm.enabled) {
// Disable the snooze if we just changed the snoozed alarm. This
// only does work if the snoozed alarm is the same as the given
// alarm.
// TODO: disableSnoozeAlert should have a better name.
disableSnoozeAlert(context, alarm.id);
// Disable the snooze if this alarm fires before the snoozed alarm.
// This works on every alarm since the user most likely intends to
// have the modified alarm fire next.
clearSnoozeIfNeeded(context, timeInMillis);
}
setNextAlert(context);
return timeInMillis;
}
/**
* A convenience method to enable or disable an alarm.
*
* @param id corresponds to the _id column
* @param enabled corresponds to the ENABLED column
*/
public static void enableAlarm(
final Context context, final int id, boolean enabled) {
enableAlarmInternal(context, id, enabled);
setNextAlert(context);
}
private static void enableAlarmInternal(final Context context,
final int id, boolean enabled) {
enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
enabled);
}
private static void enableAlarmInternal(final Context context,
final Alarm alarm, boolean enabled) {
if (alarm == null) {
return;
}
ContentResolver resolver = context.getContentResolver();
ContentValues values = new ContentValues(2);
values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
// If we are enabling the alarm, calculate alarm time since the time
// value in Alarm may be old.
if (enabled) {
long time = 0;
if (!alarm.daysOfWeek.isRepeatSet()) {
time = calculateAlarm(alarm);
}
values.put(Alarm.Columns.ALARM_TIME, time);
} else {
// Clear the snooze if the id matches.
disableSnoozeAlert(context, alarm.id);
}
resolver.update(ContentUris.withAppendedId(
Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
}
private static Alarm calculateNextAlert(final Context context) {
long minTime = Long.MAX_VALUE;
long now = System.currentTimeMillis();
final SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, 0);
Set<Alarm> alarms = new HashSet<Alarm>();
// We need to to build the list of alarms from both the snoozed list and the scheduled
// list. For a non-repeating alarm, when it goes of, it becomes disabled. A snoozed
// non-repeating alarm is not in the active list in the database.
// first go through the snoozed alarms
final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
for (String snoozedAlarm : snoozedIds) {
final int alarmId = Integer.parseInt(snoozedAlarm);
final Alarm a = getAlarm(context.getContentResolver(), alarmId);
alarms.add(a);
}
// Now add the scheduled alarms
final Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver());
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
do {
final Alarm a = new Alarm(cursor);
alarms.add(a);
} while (cursor.moveToNext());
}
} finally {
cursor.close();
}
}
Alarm alarm = null;
for (Alarm a : alarms) {
// A time of 0 indicates this is a repeating alarm, so
// calculate the time to get the next alert.
if (a.time == 0) {
a.time = calculateAlarm(a);
}
// Update the alarm if it has been snoozed
updateAlarmTimeForSnooze(prefs, a);
if (a.time < now) {
Log.v("Disabling expired alarm set for " + Log.formatTime(a.time));
// Expired alarm, disable it and move along.
enableAlarmInternal(context, a, false);
continue;
}
if (a.time < minTime) {
minTime = a.time;
alarm = a;
}
}
return alarm;
}
/**
* Disables non-repeating alarms that have passed. Called at
* boot.
*/
public static void disableExpiredAlarms(final Context context) {
Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
long now = System.currentTimeMillis();
try {
if (cur.moveToFirst()) {
do {
Alarm alarm = new Alarm(cur);
// A time of 0 means this alarm repeats. If the time is
// non-zero, check if the time is before now.
if (alarm.time != 0 && alarm.time < now) {
Log.v("Disabling expired alarm set for " +
Log.formatTime(alarm.time));
enableAlarmInternal(context, alarm, false);
}
} while (cur.moveToNext());
}
} finally {
cur.close();
}
}
/**
* Called at system startup, on time/timezone change, and whenever
* the user changes alarm settings. Activates snooze if set,
* otherwise loads all alarms, activates next alert.
*/
public static void setNextAlert(final Context context) {
final Alarm alarm = calculateNextAlert(context);
if (alarm != null) {
enableAlert(context, alarm, alarm.time);
} else {
disableAlert(context);
}
Intent i = new Intent(NEXT_ALARM_TIME_SET);
context.sendBroadcast(i);
}
/**
* Sets alert in AlarmManger and StatusBar. This is what will
* actually launch the alert when the alarm triggers.
*
* @param alarm Alarm.
* @param atTimeInMillis milliseconds since epoch
*/
private static void enableAlert(Context context, final Alarm alarm,
final long atTimeInMillis) {
AlarmManager am = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
// Intentionally verbose: always log the alarm time to provide useful
// information in bug reports.
Log.v("Alarm set for id=" + alarm.id + " " + Log.formatTime(atTimeInMillis));
Intent intent = new Intent(ALARM_ALERT_ACTION);
// XXX: This is a slight hack to avoid an exception in the remote
// AlarmManagerService process. The AlarmManager adds extra data to
// this Intent which causes it to inflate. Since the remote process
// does not know about the Alarm class, it throws a
// ClassNotFoundException.
//
// To avoid this, we marshall the data ourselves and then parcel a plain
// byte[] array. The AlarmReceiver class knows to build the Alarm
// object from the byte[] array.
Parcel out = Parcel.obtain();
alarm.writeToParcel(out, 0);
out.setDataPosition(0);
intent.putExtra(ALARM_RAW_DATA, out.marshall());
PendingIntent sender = PendingIntent.getBroadcast(
context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
setStatusBarIcon(context, true);
Calendar c = Calendar.getInstance();
c.setTimeInMillis(atTimeInMillis);
String timeString = formatDayAndTime(context, c);
saveNextAlarm(context, timeString);
}
/**
* Disables alert in AlarmManager and StatusBar.
*
* @param context The context
*/
static void disableAlert(Context context) {
AlarmManager am = (AlarmManager)
context.getSystemService(Context.ALARM_SERVICE);
PendingIntent sender = PendingIntent.getBroadcast(
context, 0, new Intent(ALARM_ALERT_ACTION),
PendingIntent.FLAG_CANCEL_CURRENT);
am.cancel(sender);
setStatusBarIcon(context, false);
// Intentionally verbose: always log the lack of a next alarm to provide useful
// information in bug reports.
Log.v("No next alarm");
saveNextAlarm(context, "");
}
static void saveSnoozeAlert(final Context context, final int id,
final long time) {
SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, 0);
if (id == INVALID_ALARM_ID) {
clearAllSnoozePreferences(context, prefs);
} else {
final Set<String> snoozedIds =
prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
snoozedIds.add(Integer.toString(id));
final SharedPreferences.Editor ed = prefs.edit();
ed.putStringSet(PREF_SNOOZE_IDS, snoozedIds);
ed.putLong(getAlarmPrefSnoozeTimeKey(id), time);
ed.apply();
}
// Set the next alert after updating the snooze.
setNextAlert(context);
}
private static String getAlarmPrefSnoozeTimeKey(int id) {
return getAlarmPrefSnoozeTimeKey(Integer.toString(id));
}
private static String getAlarmPrefSnoozeTimeKey(String id) {
return PREF_SNOOZE_TIME + id;
}
/**
* Disable the snooze alert if the given id matches the snooze id.
*/
static void disableSnoozeAlert(final Context context, final int id) {
SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, 0);
if (hasAlarmBeenSnoozed(prefs, id)) {
// This is the same id so clear the shared prefs.
clearSnoozePreference(context, prefs, id);
}
}
// Helper to remove the snooze preference. Do not use clear because that
// will erase the clock preferences. Also clear the snooze notification in
// the window shade.
private static void clearSnoozePreference(final Context context,
final SharedPreferences prefs, final int id) {
final String alarmStr = Integer.toString(id);
final Set<String> snoozedIds =
prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
if (snoozedIds.contains(alarmStr)) {
NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.cancel(id);
}
final SharedPreferences.Editor ed = prefs.edit();
snoozedIds.remove(alarmStr);
ed.putStringSet(PREF_SNOOZE_IDS, snoozedIds);
ed.remove(getAlarmPrefSnoozeTimeKey(alarmStr));
ed.apply();
}
private static void clearAllSnoozePreferences(final Context context,
final SharedPreferences prefs) {
NotificationManager nm = (NotificationManager)
context.getSystemService(Context.NOTIFICATION_SERVICE);
final Set<String> snoozedIds =
prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
final SharedPreferences.Editor ed = prefs.edit();
for (String snoozeId : snoozedIds) {
nm.cancel(Integer.parseInt(snoozeId));
ed.remove(getAlarmPrefSnoozeTimeKey(snoozeId));
}
ed.remove(PREF_SNOOZE_IDS);
ed.apply();
}
private static boolean hasAlarmBeenSnoozed(final SharedPreferences prefs, final int alarmId) {
final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, null);
// Return true if there a valid snoozed alarmId was saved
return snoozedIds != null && snoozedIds.contains(Integer.toString(alarmId));
}
/**
* Updates the specified Alarm with the additional snooze time.
* Returns a boolean indicating whether the alarm was updated.
*/
private static boolean updateAlarmTimeForSnooze(
final SharedPreferences prefs, final Alarm alarm) {
if (!hasAlarmBeenSnoozed(prefs, alarm.id)) {
// No need to modify the alarm
return false;
}
final long time = prefs.getLong(getAlarmPrefSnoozeTimeKey(alarm.id), -1);
// The time in the database is either 0 (repeating) or a specific time
// for a non-repeating alarm. Update this value so the AlarmReceiver
// has the right time to compare.
alarm.time = time;
return true;
}
/**
* Tells the StatusBar whether the alarm is enabled or disabled
*/
private static void setStatusBarIcon(Context context, boolean enabled) {
Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
alarmChanged.putExtra("alarmSet", enabled);
context.sendBroadcast(alarmChanged);
}
private static long calculateAlarm(Alarm alarm) {
return calculateAlarm(alarm.hour, alarm.minutes, alarm.daysOfWeek)
.getTimeInMillis();
}
/**
* Given an alarm in hours and minutes, return a time suitable for
* setting in AlarmManager.
*/
static Calendar calculateAlarm(int hour, int minute,
Alarm.DaysOfWeek daysOfWeek) {
// start with now
Calendar c = Calendar.getInstance();
c.setTimeInMillis(System.currentTimeMillis());
int nowHour = c.get(Calendar.HOUR_OF_DAY);
int nowMinute = c.get(Calendar.MINUTE);
// if alarm is behind current time, advance one day
if (hour < nowHour ||
hour == nowHour && minute <= nowMinute) {
c.add(Calendar.DAY_OF_YEAR, 1);
}
c.set(Calendar.HOUR_OF_DAY, hour);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
c.set(Calendar.MILLISECOND, 0);
int addDays = daysOfWeek.getNextAlarm(c);
if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
return c;
}
static String formatTime(final Context context, int hour, int minute,
Alarm.DaysOfWeek daysOfWeek) {
Calendar c = calculateAlarm(hour, minute, daysOfWeek);
return formatTime(context, c);
}
/* used by AlarmAlert */
static String formatTime(final Context context, Calendar c) {
String format = get24HourMode(context) ? M24 : M12;
return (c == null) ? "" : (String)DateFormat.format(format, c);
}
/**
* Shows day and time -- used for lock screen
*/
private static String formatDayAndTime(final Context context, Calendar c) {
String format = get24HourMode(context) ? DM24 : DM12;
return (c == null) ? "" : (String)DateFormat.format(format, c);
}
/**
* Save time of the next alarm, as a formatted string, into the system
* settings so those who care can make use of it.
*/
static void saveNextAlarm(final Context context, String timeString) {
Log.v("Setting next alarm string in system to " +
(TextUtils.isEmpty(timeString) ? "null" : timeString));
Settings.System.putString(context.getContentResolver(),
Settings.System.NEXT_ALARM_FORMATTED,
timeString);
}
/**
* @return true if clock is set to 24-hour mode
*/
public static boolean get24HourMode(final Context context) {
return android.text.format.DateFormat.is24HourFormat(context);
}
}