• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
addAlarm(Context context, Alarm alarm)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      */
deleteAlarm(Context context, int alarmId)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      */
getAlarmsCursor(ContentResolver contentResolver)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.
getFilteredAlarmsCursor( ContentResolver contentResolver)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 
createContentValues(Alarm alarm)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 
clearSnoozeIfNeeded(Context context, long alarmTime)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      */
getAlarm(ContentResolver contentResolver, int alarmId)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      */
setAlarm(Context context, Alarm alarm)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 
enableAlarm( final Context context, final int id, boolean enabled)243     public static void enableAlarm(
244             final Context context, final int id, boolean enabled) {
245         enableAlarmInternal(context, id, enabled);
246         setNextAlert(context);
247     }
248 
enableAlarmInternal(final Context context, final int id, boolean enabled)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 
enableAlarmInternal(final Context context, final Alarm alarm, boolean enabled)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 
calculateNextAlert(final Context context)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      */
disableExpiredAlarms(final Context context)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      */
setNextAlert(final Context context)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      */
enableAlert(Context context, final Alarm alarm, final long atTimeInMillis)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      */
disableAlert(Context context)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 
saveSnoozeAlert(final Context context, final int id, final long time)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      */
disableSnoozeAlert(final Context context, final int id)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.
clearSnoozePreference(final Context context, final SharedPreferences prefs)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      */
enableSnoozeAlert(final Context context)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      */
setStatusBarIcon(Context context, boolean enabled)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 
calculateAlarm(Alarm alarm)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      */
calculateAlarm(int hour, int minute, Alarm.DaysOfWeek daysOfWeek)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 
formatTime(final Context context, int hour, int minute, Alarm.DaysOfWeek daysOfWeek)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 */
formatTime(final Context context, Calendar c)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      */
formatDayAndTime(final Context context, Calendar c)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      */
saveNextAlarm(final Context context, String timeString)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      */
get24HourMode(final Context context)567     static boolean get24HourMode(final Context context) {
568         return android.text.format.DateFormat.is24HourFormat(context);
569     }
570 }
571