Home | History | Annotate | Download | only in deskclock
      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