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.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.util.HashSet;
36import java.util.Set;
37
38/**
39 * The Alarms provider supplies info about Alarm Clock settings
40 */
41public class Alarms {
42
43    // This action triggers the AlarmReceiver as well as the AlarmKlaxon. It
44    // is a public action used in the manifest for receiving Alarm broadcasts
45    // from the alarm manager.
46    public static final String ALARM_ALERT_ACTION = "com.android.deskclock.ALARM_ALERT";
47
48    // A public action sent by AlarmKlaxon when the alarm has stopped sounding
49    // for any reason (e.g. because it has been dismissed from AlarmAlertFullScreen,
50    // or killed due to an incoming phone call, etc).
51    public static final String ALARM_DONE_ACTION = "com.android.deskclock.ALARM_DONE";
52
53    // AlarmAlertFullScreen listens for this broadcast intent, so that other applications
54    // can snooze the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
55    public static final String ALARM_SNOOZE_ACTION = "com.android.deskclock.ALARM_SNOOZE";
56
57    // AlarmAlertFullScreen listens for this broadcast intent, so that other applications
58    // can dismiss the alarm (after ALARM_ALERT_ACTION and before ALARM_DONE_ACTION).
59    public static final String ALARM_DISMISS_ACTION = "com.android.deskclock.ALARM_DISMISS";
60
61    // This is a private action used by the AlarmKlaxon to update the UI to
62    // show the alarm has been killed.
63    public static final String ALARM_KILLED = "alarm_killed";
64
65    // Extra in the ALARM_KILLED intent to indicate to the user how long the
66    // alarm played before being killed.
67    public static final String ALARM_KILLED_TIMEOUT = "alarm_killed_timeout";
68
69    // This string is used to indicate a silent alarm in the db.
70    public static final String ALARM_ALERT_SILENT = "silent";
71
72    // This intent is sent from the notification when the user cancels the
73    // snooze alert.
74    public static final String CANCEL_SNOOZE = "cancel_snooze";
75
76    // This string is used when passing an Alarm object through an intent.
77    public static final String ALARM_INTENT_EXTRA = "intent.extra.alarm";
78
79    // This extra is the raw Alarm object data. It is used in the
80    // AlarmManagerService to avoid a ClassNotFoundException when filling in
81    // the Intent extras.
82    public static final String ALARM_RAW_DATA = "intent.extra.alarm_raw";
83
84    private static final String PREF_SNOOZE_IDS = "snooze_ids";
85    private static final String PREF_SNOOZE_TIME = "snooze_time";
86
87    private final static String DM12 = "E h:mm aa";
88    private final static String DM24 = "E kk:mm";
89
90    private final static String M12 = "h:mm aa";
91    // Shared with DigitalClock
92    final static String M24 = "kk:mm";
93
94    final static int INVALID_ALARM_ID = -1;
95
96    /**
97     * Creates a new Alarm and fills in the given alarm's id.
98     */
99    public static long addAlarm(Context context, Alarm alarm) {
100        ContentValues values = createContentValues(alarm);
101        Uri uri = context.getContentResolver().insert(
102                Alarm.Columns.CONTENT_URI, values);
103        alarm.id = (int) ContentUris.parseId(uri);
104
105        long timeInMillis = calculateAlarm(alarm);
106        if (alarm.enabled) {
107            clearSnoozeIfNeeded(context, timeInMillis);
108        }
109        setNextAlert(context);
110        return timeInMillis;
111    }
112
113    /**
114     * Removes an existing Alarm.  If this alarm is snoozing, disables
115     * snooze.  Sets next alert.
116     */
117    public static void deleteAlarm(Context context, int alarmId) {
118        if (alarmId == INVALID_ALARM_ID) return;
119
120        ContentResolver contentResolver = context.getContentResolver();
121        /* If alarm is snoozing, lose it */
122        disableSnoozeAlert(context, alarmId);
123
124        Uri uri = ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId);
125        contentResolver.delete(uri, "", null);
126
127        setNextAlert(context);
128    }
129
130    /**
131     * Queries all alarms
132     * @return cursor over all alarms
133     */
134    public static Cursor getAlarmsCursor(ContentResolver contentResolver) {
135        return contentResolver.query(
136                Alarm.Columns.CONTENT_URI, Alarm.Columns.ALARM_QUERY_COLUMNS,
137                null, null, Alarm.Columns.DEFAULT_SORT_ORDER);
138    }
139
140    // Private method to get a more limited set of alarms from the database.
141    private static Cursor getFilteredAlarmsCursor(
142            ContentResolver contentResolver) {
143        return contentResolver.query(Alarm.Columns.CONTENT_URI,
144                Alarm.Columns.ALARM_QUERY_COLUMNS, Alarm.Columns.WHERE_ENABLED,
145                null, null);
146    }
147
148    private static ContentValues createContentValues(Alarm alarm) {
149        ContentValues values = new ContentValues(8);
150        // Set the alarm_time value if this alarm does not repeat. This will be
151        // used later to disable expire alarms.
152        long time = 0;
153        if (!alarm.daysOfWeek.isRepeatSet()) {
154            time = calculateAlarm(alarm);
155        }
156
157        values.put(Alarm.Columns.ENABLED, alarm.enabled ? 1 : 0);
158        values.put(Alarm.Columns.HOUR, alarm.hour);
159        values.put(Alarm.Columns.MINUTES, alarm.minutes);
160        values.put(Alarm.Columns.ALARM_TIME, time);
161        values.put(Alarm.Columns.DAYS_OF_WEEK, alarm.daysOfWeek.getCoded());
162        values.put(Alarm.Columns.VIBRATE, alarm.vibrate);
163        values.put(Alarm.Columns.MESSAGE, alarm.label);
164
165        // A null alert Uri indicates a silent alarm.
166        values.put(Alarm.Columns.ALERT, alarm.alert == null ? ALARM_ALERT_SILENT
167                : alarm.alert.toString());
168
169        return values;
170    }
171
172    private static void clearSnoozeIfNeeded(Context context, long alarmTime) {
173        // If this alarm fires before the next snooze, clear the snooze to
174        // enable this alarm.
175        SharedPreferences prefs = context.getSharedPreferences(AlarmClock.PREFERENCES, 0);
176
177        // Get the list of snoozed alarms
178        final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
179        for (String snoozedAlarm : snoozedIds) {
180            final long snoozeTime = prefs.getLong(getAlarmPrefSnoozeTimeKey(snoozedAlarm), 0);
181            if (alarmTime < snoozeTime) {
182                final int alarmId = Integer.parseInt(snoozedAlarm);
183                clearSnoozePreference(context, prefs, alarmId);
184            }
185        }
186    }
187
188    /**
189     * Return an Alarm object representing the alarm id in the database.
190     * Returns null if no alarm exists.
191     */
192    public static Alarm getAlarm(ContentResolver contentResolver, int alarmId) {
193        Cursor cursor = contentResolver.query(
194                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarmId),
195                Alarm.Columns.ALARM_QUERY_COLUMNS,
196                null, null, null);
197        Alarm alarm = null;
198        if (cursor != null) {
199            if (cursor.moveToFirst()) {
200                alarm = new Alarm(cursor);
201            }
202            cursor.close();
203        }
204        return alarm;
205    }
206
207
208    /**
209     * A convenience method to set an alarm in the Alarms
210     * content provider.
211     * @return Time when the alarm will fire.
212     */
213    public static long setAlarm(Context context, Alarm alarm) {
214        ContentValues values = createContentValues(alarm);
215        ContentResolver resolver = context.getContentResolver();
216        resolver.update(
217                ContentUris.withAppendedId(Alarm.Columns.CONTENT_URI, alarm.id),
218                values, null, null);
219
220        long timeInMillis = calculateAlarm(alarm);
221
222        if (alarm.enabled) {
223            // Disable the snooze if we just changed the snoozed alarm. This
224            // only does work if the snoozed alarm is the same as the given
225            // alarm.
226            // TODO: disableSnoozeAlert should have a better name.
227            disableSnoozeAlert(context, alarm.id);
228
229            // Disable the snooze if this alarm fires before the snoozed alarm.
230            // This works on every alarm since the user most likely intends to
231            // have the modified alarm fire next.
232            clearSnoozeIfNeeded(context, timeInMillis);
233        }
234
235        setNextAlert(context);
236
237        return timeInMillis;
238    }
239
240    /**
241     * A convenience method to enable or disable an alarm.
242     *
243     * @param id             corresponds to the _id column
244     * @param enabled        corresponds to the ENABLED column
245     */
246
247    public static void enableAlarm(
248            final Context context, final int id, boolean enabled) {
249        enableAlarmInternal(context, id, enabled);
250        setNextAlert(context);
251    }
252
253    private static void enableAlarmInternal(final Context context,
254            final int id, boolean enabled) {
255        enableAlarmInternal(context, getAlarm(context.getContentResolver(), id),
256                enabled);
257    }
258
259    private static void enableAlarmInternal(final Context context,
260            final Alarm alarm, boolean enabled) {
261        if (alarm == null) {
262            return;
263        }
264        ContentResolver resolver = context.getContentResolver();
265
266        ContentValues values = new ContentValues(2);
267        values.put(Alarm.Columns.ENABLED, enabled ? 1 : 0);
268
269        // If we are enabling the alarm, calculate alarm time since the time
270        // value in Alarm may be old.
271        if (enabled) {
272            long time = 0;
273            if (!alarm.daysOfWeek.isRepeatSet()) {
274                time = calculateAlarm(alarm);
275            }
276            values.put(Alarm.Columns.ALARM_TIME, time);
277        } else {
278            // Clear the snooze if the id matches.
279            disableSnoozeAlert(context, alarm.id);
280        }
281
282        resolver.update(ContentUris.withAppendedId(
283                Alarm.Columns.CONTENT_URI, alarm.id), values, null, null);
284    }
285
286    private static Alarm calculateNextAlert(final Context context) {
287        long minTime = Long.MAX_VALUE;
288        long now = System.currentTimeMillis();
289        final SharedPreferences prefs = context.getSharedPreferences(AlarmClock.PREFERENCES, 0);
290
291        Set<Alarm> alarms = new HashSet<Alarm>();
292
293        // We need to to build the list of alarms from both the snoozed list and the scheduled
294        // list.  For a non-repeating alarm, when it goes of, it becomes disabled.  A snoozed
295        // non-repeating alarm is not in the active list in the database.
296
297        // first go through the snoozed alarms
298        final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
299        for (String snoozedAlarm : snoozedIds) {
300            final int alarmId = Integer.parseInt(snoozedAlarm);
301            final Alarm a = getAlarm(context.getContentResolver(), alarmId);
302            alarms.add(a);
303        }
304
305        // Now add the scheduled alarms
306        final Cursor cursor = getFilteredAlarmsCursor(context.getContentResolver());
307        if (cursor != null) {
308            try {
309                if (cursor.moveToFirst()) {
310                    do {
311                        final Alarm a = new Alarm(cursor);
312                        alarms.add(a);
313                    } while (cursor.moveToNext());
314                }
315            } finally {
316                cursor.close();
317            }
318        }
319
320        Alarm alarm = null;
321
322        for (Alarm a : alarms) {
323            // A time of 0 indicates this is a repeating alarm, so
324            // calculate the time to get the next alert.
325            if (a.time == 0) {
326                a.time = calculateAlarm(a);
327            }
328
329            // Update the alarm if it has been snoozed
330            updateAlarmTimeForSnooze(prefs, a);
331
332            if (a.time < now) {
333                Log.v("Disabling expired alarm set for " + Log.formatTime(a.time));
334                // Expired alarm, disable it and move along.
335                enableAlarmInternal(context, a, false);
336                continue;
337            }
338            if (a.time < minTime) {
339                minTime = a.time;
340                alarm = a;
341            }
342        }
343
344        return alarm;
345    }
346
347    /**
348     * Disables non-repeating alarms that have passed.  Called at
349     * boot.
350     */
351    public static void disableExpiredAlarms(final Context context) {
352        Cursor cur = getFilteredAlarmsCursor(context.getContentResolver());
353        long now = System.currentTimeMillis();
354
355        try {
356            if (cur.moveToFirst()) {
357                do {
358                    Alarm alarm = new Alarm(cur);
359                    // A time of 0 means this alarm repeats. If the time is
360                    // non-zero, check if the time is before now.
361                    if (alarm.time != 0 && alarm.time < now) {
362                        Log.v("Disabling expired alarm set for " +
363                              Log.formatTime(alarm.time));
364                        enableAlarmInternal(context, alarm, false);
365                    }
366                } while (cur.moveToNext());
367            }
368        } finally {
369            cur.close();
370        }
371    }
372
373    /**
374     * Called at system startup, on time/timezone change, and whenever
375     * the user changes alarm settings.  Activates snooze if set,
376     * otherwise loads all alarms, activates next alert.
377     */
378    public static void setNextAlert(final Context context) {
379        final Alarm alarm = calculateNextAlert(context);
380        if (alarm != null) {
381            enableAlert(context, alarm, alarm.time);
382        } else {
383            disableAlert(context);
384        }
385    }
386
387    /**
388     * Sets alert in AlarmManger and StatusBar.  This is what will
389     * actually launch the alert when the alarm triggers.
390     *
391     * @param alarm Alarm.
392     * @param atTimeInMillis milliseconds since epoch
393     */
394    private static void enableAlert(Context context, final Alarm alarm,
395            final long atTimeInMillis) {
396        AlarmManager am = (AlarmManager)
397                context.getSystemService(Context.ALARM_SERVICE);
398
399        if (Log.LOGV) {
400            Log.v("** setAlert id " + alarm.id + " atTime " + atTimeInMillis);
401        }
402
403        Intent intent = new Intent(ALARM_ALERT_ACTION);
404
405        // XXX: This is a slight hack to avoid an exception in the remote
406        // AlarmManagerService process. The AlarmManager adds extra data to
407        // this Intent which causes it to inflate. Since the remote process
408        // does not know about the Alarm class, it throws a
409        // ClassNotFoundException.
410        //
411        // To avoid this, we marshall the data ourselves and then parcel a plain
412        // byte[] array. The AlarmReceiver class knows to build the Alarm
413        // object from the byte[] array.
414        Parcel out = Parcel.obtain();
415        alarm.writeToParcel(out, 0);
416        out.setDataPosition(0);
417        intent.putExtra(ALARM_RAW_DATA, out.marshall());
418
419        PendingIntent sender = PendingIntent.getBroadcast(
420                context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
421
422        am.set(AlarmManager.RTC_WAKEUP, atTimeInMillis, sender);
423
424        setStatusBarIcon(context, true);
425
426        Calendar c = Calendar.getInstance();
427        c.setTimeInMillis(atTimeInMillis);
428        String timeString = formatDayAndTime(context, c);
429        saveNextAlarm(context, timeString);
430    }
431
432    /**
433     * Disables alert in AlarmManger and StatusBar.
434     *
435     * @param id Alarm ID.
436     */
437    static void disableAlert(Context context) {
438        AlarmManager am = (AlarmManager)
439                context.getSystemService(Context.ALARM_SERVICE);
440        PendingIntent sender = PendingIntent.getBroadcast(
441                context, 0, new Intent(ALARM_ALERT_ACTION),
442                PendingIntent.FLAG_CANCEL_CURRENT);
443        am.cancel(sender);
444        setStatusBarIcon(context, false);
445        saveNextAlarm(context, "");
446    }
447
448    static void saveSnoozeAlert(final Context context, final int id,
449            final long time) {
450        SharedPreferences prefs = context.getSharedPreferences(
451                AlarmClock.PREFERENCES, 0);
452        if (id == INVALID_ALARM_ID) {
453            clearAllSnoozePreferences(context, prefs);
454        } else {
455            final Set<String> snoozedIds =
456                    prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
457            snoozedIds.add(Integer.toString(id));
458            final SharedPreferences.Editor ed = prefs.edit();
459            ed.putStringSet(PREF_SNOOZE_IDS, snoozedIds);
460            ed.putLong(getAlarmPrefSnoozeTimeKey(id), time);
461            ed.apply();
462        }
463        // Set the next alert after updating the snooze.
464        setNextAlert(context);
465    }
466
467    private static String getAlarmPrefSnoozeTimeKey(int id) {
468        return getAlarmPrefSnoozeTimeKey(Integer.toString(id));
469    }
470
471    private static String getAlarmPrefSnoozeTimeKey(String id) {
472        return PREF_SNOOZE_TIME + id;
473    }
474
475    /**
476     * Disable the snooze alert if the given id matches the snooze id.
477     */
478    static void disableSnoozeAlert(final Context context, final int id) {
479        SharedPreferences prefs = context.getSharedPreferences(
480                AlarmClock.PREFERENCES, 0);
481        if (hasAlarmBeenSnoozed(prefs, id)) {
482            // This is the same id so clear the shared prefs.
483            clearSnoozePreference(context, prefs, id);
484        }
485    }
486
487    // Helper to remove the snooze preference. Do not use clear because that
488    // will erase the clock preferences. Also clear the snooze notification in
489    // the window shade.
490    private static void clearSnoozePreference(final Context context,
491            final SharedPreferences prefs, final int id) {
492        final String alarmStr = Integer.toString(id);
493        final Set<String> snoozedIds =
494                prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
495        if (snoozedIds.contains(alarmStr)) {
496            NotificationManager nm = (NotificationManager)
497                    context.getSystemService(Context.NOTIFICATION_SERVICE);
498            nm.cancel(id);
499        }
500
501        final SharedPreferences.Editor ed = prefs.edit();
502        snoozedIds.remove(alarmStr);
503        ed.putStringSet(PREF_SNOOZE_IDS, snoozedIds);
504        ed.remove(getAlarmPrefSnoozeTimeKey(alarmStr));
505        ed.apply();
506    }
507
508    private static void clearAllSnoozePreferences(final Context context,
509            final SharedPreferences prefs) {
510        NotificationManager nm = (NotificationManager)
511                context.getSystemService(Context.NOTIFICATION_SERVICE);
512        final Set<String> snoozedIds =
513                prefs.getStringSet(PREF_SNOOZE_IDS, new HashSet<String>());
514        final SharedPreferences.Editor ed = prefs.edit();
515        for (String snoozeId : snoozedIds) {
516            nm.cancel(Integer.parseInt(snoozeId));
517            ed.remove(getAlarmPrefSnoozeTimeKey(snoozeId));
518        }
519
520        ed.remove(PREF_SNOOZE_IDS);
521        ed.apply();
522    }
523
524    private static boolean hasAlarmBeenSnoozed(final SharedPreferences prefs, final int alarmId) {
525        final Set<String> snoozedIds = prefs.getStringSet(PREF_SNOOZE_IDS, null);
526
527        // Return true if there a valid snoozed alarmId was saved
528        return snoozedIds != null && snoozedIds.contains(Integer.toString(alarmId));
529    }
530
531    /**
532     * Updates the specified Alarm with the additional snooze time.
533     * Returns a boolean indicating whether the alarm was updated.
534     */
535    private static boolean updateAlarmTimeForSnooze(
536            final SharedPreferences prefs, final Alarm alarm) {
537        if (!hasAlarmBeenSnoozed(prefs, alarm.id)) {
538            // No need to modify the alarm
539            return false;
540        }
541
542        final long time = prefs.getLong(getAlarmPrefSnoozeTimeKey(alarm.id), -1);
543        // The time in the database is either 0 (repeating) or a specific time
544        // for a non-repeating alarm. Update this value so the AlarmReceiver
545        // has the right time to compare.
546        alarm.time = time;
547
548        return true;
549    }
550
551    /**
552     * Tells the StatusBar whether the alarm is enabled or disabled
553     */
554    private static void setStatusBarIcon(Context context, boolean enabled) {
555        Intent alarmChanged = new Intent("android.intent.action.ALARM_CHANGED");
556        alarmChanged.putExtra("alarmSet", enabled);
557        context.sendBroadcast(alarmChanged);
558    }
559
560    private static long calculateAlarm(Alarm alarm) {
561        return calculateAlarm(alarm.hour, alarm.minutes, alarm.daysOfWeek)
562                .getTimeInMillis();
563    }
564
565    /**
566     * Given an alarm in hours and minutes, return a time suitable for
567     * setting in AlarmManager.
568     */
569    static Calendar calculateAlarm(int hour, int minute,
570            Alarm.DaysOfWeek daysOfWeek) {
571
572        // start with now
573        Calendar c = Calendar.getInstance();
574        c.setTimeInMillis(System.currentTimeMillis());
575
576        int nowHour = c.get(Calendar.HOUR_OF_DAY);
577        int nowMinute = c.get(Calendar.MINUTE);
578
579        // if alarm is behind current time, advance one day
580        if (hour < nowHour  ||
581            hour == nowHour && minute <= nowMinute) {
582            c.add(Calendar.DAY_OF_YEAR, 1);
583        }
584        c.set(Calendar.HOUR_OF_DAY, hour);
585        c.set(Calendar.MINUTE, minute);
586        c.set(Calendar.SECOND, 0);
587        c.set(Calendar.MILLISECOND, 0);
588
589        int addDays = daysOfWeek.getNextAlarm(c);
590        if (addDays > 0) c.add(Calendar.DAY_OF_WEEK, addDays);
591        return c;
592    }
593
594    static String formatTime(final Context context, int hour, int minute,
595                             Alarm.DaysOfWeek daysOfWeek) {
596        Calendar c = calculateAlarm(hour, minute, daysOfWeek);
597        return formatTime(context, c);
598    }
599
600    /* used by AlarmAlert */
601    static String formatTime(final Context context, Calendar c) {
602        String format = get24HourMode(context) ? M24 : M12;
603        return (c == null) ? "" : (String)DateFormat.format(format, c);
604    }
605
606    /**
607     * Shows day and time -- used for lock screen
608     */
609    private static String formatDayAndTime(final Context context, Calendar c) {
610        String format = get24HourMode(context) ? DM24 : DM12;
611        return (c == null) ? "" : (String)DateFormat.format(format, c);
612    }
613
614    /**
615     * Save time of the next alarm, as a formatted string, into the system
616     * settings so those who care can make use of it.
617     */
618    static void saveNextAlarm(final Context context, String timeString) {
619        Settings.System.putString(context.getContentResolver(),
620                                  Settings.System.NEXT_ALARM_FORMATTED,
621                                  timeString);
622    }
623
624    /**
625     * @return true if clock is set to 24-hour mode
626     */
627    static boolean get24HourMode(final Context context) {
628        return android.text.format.DateFormat.is24HourFormat(context);
629    }
630}
631