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