Alarm.java revision 900ede28311ddf4502622009a3d5ab44a11b7264
1/*
2 * Copyright (C) 2013 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.provider;
18
19import android.content.ContentResolver;
20import android.content.ContentUris;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.CursorLoader;
24import android.content.Intent;
25import android.database.Cursor;
26import android.media.RingtoneManager;
27import android.net.Uri;
28import android.os.Parcel;
29import android.os.Parcelable;
30
31import com.android.deskclock.R;
32import com.android.deskclock.data.DataModel;
33import com.android.deskclock.data.Weekdays;
34
35import java.util.Calendar;
36import java.util.LinkedList;
37import java.util.List;
38
39public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
40    /**
41     * Alarms start with an invalid id when it hasn't been saved to the database.
42     */
43    public static final long INVALID_ID = -1;
44
45    /**
46     * The default sort order for this table
47     */
48    private static final String DEFAULT_SORT_ORDER =
49            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " +
50            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +  MINUTES + " ASC" + ", " +
51            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC";
52
53    private static final String[] QUERY_COLUMNS = {
54            _ID,
55            HOUR,
56            MINUTES,
57            DAYS_OF_WEEK,
58            ENABLED,
59            VIBRATE,
60            LABEL,
61            RINGTONE,
62            DELETE_AFTER_USE
63    };
64
65    private static final String[] QUERY_ALARMS_WITH_INSTANCES_COLUMNS = {
66            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID,
67            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR,
68            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES,
69            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK,
70            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED,
71            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE,
72            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL,
73            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE,
74            ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE,
75            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "."
76                    + ClockContract.InstancesColumns.ALARM_STATE,
77            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns._ID,
78            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.YEAR,
79            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MONTH,
80            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.DAY,
81            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.HOUR,
82            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES,
83            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL,
84            ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE
85    };
86
87    /**
88     * These save calls to cursor.getColumnIndexOrThrow()
89     * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
90     */
91    private static final int ID_INDEX = 0;
92    private static final int HOUR_INDEX = 1;
93    private static final int MINUTES_INDEX = 2;
94    private static final int DAYS_OF_WEEK_INDEX = 3;
95    private static final int ENABLED_INDEX = 4;
96    private static final int VIBRATE_INDEX = 5;
97    private static final int LABEL_INDEX = 6;
98    private static final int RINGTONE_INDEX = 7;
99    private static final int DELETE_AFTER_USE_INDEX = 8;
100    private static final int INSTANCE_STATE_INDEX = 9;
101    public static final int INSTANCE_ID_INDEX = 10;
102    public static final int INSTANCE_YEAR_INDEX = 11;
103    public static final int INSTANCE_MONTH_INDEX = 12;
104    public static final int INSTANCE_DAY_INDEX = 13;
105    public static final int INSTANCE_HOUR_INDEX = 14;
106    public static final int INSTANCE_MINUTE_INDEX = 15;
107    public static final int INSTANCE_LABEL_INDEX = 16;
108    public static final int INSTANCE_VIBRATE_INDEX = 17;
109
110    private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
111    private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1;
112
113    public static ContentValues createContentValues(Alarm alarm) {
114        ContentValues values = new ContentValues(COLUMN_COUNT);
115        if (alarm.id != INVALID_ID) {
116            values.put(ClockContract.AlarmsColumns._ID, alarm.id);
117        }
118
119        values.put(ENABLED, alarm.enabled ? 1 : 0);
120        values.put(HOUR, alarm.hour);
121        values.put(MINUTES, alarm.minutes);
122        values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBits());
123        values.put(VIBRATE, alarm.vibrate ? 1 : 0);
124        values.put(LABEL, alarm.label);
125        values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
126        if (alarm.alert == null) {
127            // We want to put null, so default alarm changes
128            values.putNull(RINGTONE);
129        } else {
130            values.put(RINGTONE, alarm.alert.toString());
131        }
132
133        return values;
134    }
135
136    public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
137        return new Intent(context, cls).setData(getContentUri(alarmId));
138    }
139
140    public static Uri getContentUri(long alarmId) {
141        return ContentUris.withAppendedId(CONTENT_URI, alarmId);
142    }
143
144    public static long getId(Uri contentUri) {
145        return ContentUris.parseId(contentUri);
146    }
147
148    /**
149     * Get alarm cursor loader for all alarms.
150     *
151     * @param context to query the database.
152     * @return cursor loader with all the alarms.
153     */
154    public static CursorLoader getAlarmsCursorLoader(Context context) {
155        return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
156                QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) {
157            @Override
158            public Cursor loadInBackground() {
159                // Prime the ringtone title cache for later access. Most alarms will refer to
160                // system ringtones.
161                DataModel.getDataModel().loadRingtoneTitles();
162
163                return super.loadInBackground();
164            }
165        };
166    }
167
168    /**
169     * Get alarm by id.
170     *
171     * @param cr provides access to the content model
172     * @param alarmId for the desired alarm.
173     * @return alarm if found, null otherwise
174     */
175    public static Alarm getAlarm(ContentResolver cr, long alarmId) {
176        try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) {
177            if (cursor.moveToFirst()) {
178                return new Alarm(cursor);
179            }
180        }
181
182        return null;
183    }
184    /**
185     * Get alarm for the {@code contentUri}.
186     *
187     * @param cr provides access to the content model
188     * @param contentUri the {@link #getContentUri deeplink} for the desired alarm
189     * @return instance if found, null otherwise
190     */
191    public static Alarm getAlarm(ContentResolver cr, Uri contentUri) {
192        return getAlarm(cr, ContentUris.parseId(contentUri));
193    }
194
195    /**
196     * Get all alarms given conditions.
197     *
198     * @param cr provides access to the content model
199     * @param selection A filter declaring which rows to return, formatted as an
200     *         SQL WHERE clause (excluding the WHERE itself). Passing null will
201     *         return all rows for the given URI.
202     * @param selectionArgs You may include ?s in selection, which will be
203     *         replaced by the values from selectionArgs, in the order that they
204     *         appear in the selection. The values will be bound as Strings.
205     * @return list of alarms matching where clause or empty list if none found.
206     */
207    public static List<Alarm> getAlarms(ContentResolver cr, String selection,
208            String... selectionArgs) {
209        final List<Alarm> result = new LinkedList<>();
210        try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
211            if (cursor != null && cursor.moveToFirst()) {
212                do {
213                    result.add(new Alarm(cursor));
214                } while (cursor.moveToNext());
215            }
216        }
217
218        return result;
219    }
220
221    public static boolean isTomorrow(Alarm alarm, Calendar now) {
222        if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) {
223            return false;
224        }
225
226        final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes;
227        final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
228        return totalAlarmMinutes <= totalNowMinutes;
229    }
230
231    public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
232        ContentValues values = createContentValues(alarm);
233        Uri uri = contentResolver.insert(CONTENT_URI, values);
234        alarm.id = getId(uri);
235        return alarm;
236    }
237
238    public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
239        if (alarm.id == Alarm.INVALID_ID) return false;
240        ContentValues values = createContentValues(alarm);
241        long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null);
242        return rowsUpdated == 1;
243    }
244
245    public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
246        if (alarmId == INVALID_ID) return false;
247        int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null);
248        return deletedRows == 1;
249    }
250
251    public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
252        public Alarm createFromParcel(Parcel p) {
253            return new Alarm(p);
254        }
255
256        public Alarm[] newArray(int size) {
257            return new Alarm[size];
258        }
259    };
260
261    // Public fields
262    // TODO: Refactor instance names
263    public long id;
264    public boolean enabled;
265    public int hour;
266    public int minutes;
267    public Weekdays daysOfWeek;
268    public boolean vibrate;
269    public String label;
270    public Uri alert;
271    public boolean deleteAfterUse;
272    public int instanceState;
273    public int instanceId;
274
275    // Creates a default alarm at the current time.
276    public Alarm() {
277        this(0, 0);
278    }
279
280    public Alarm(int hour, int minutes) {
281        this.id = INVALID_ID;
282        this.hour = hour;
283        this.minutes = minutes;
284        this.vibrate = true;
285        this.daysOfWeek = Weekdays.NONE;
286        this.label = "";
287        this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
288        this.deleteAfterUse = false;
289    }
290
291    public Alarm(Cursor c) {
292        id = c.getLong(ID_INDEX);
293        enabled = c.getInt(ENABLED_INDEX) == 1;
294        hour = c.getInt(HOUR_INDEX);
295        minutes = c.getInt(MINUTES_INDEX);
296        daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX));
297        vibrate = c.getInt(VIBRATE_INDEX) == 1;
298        label = c.getString(LABEL_INDEX);
299        deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
300
301        if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
302            instanceState = c.getInt(INSTANCE_STATE_INDEX);
303            instanceId = c.getInt(INSTANCE_ID_INDEX);
304        }
305
306        if (c.isNull(RINGTONE_INDEX)) {
307            // Should we be saving this with the current ringtone or leave it null
308            // so it changes when user changes default ringtone?
309            alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
310        } else {
311            alert = Uri.parse(c.getString(RINGTONE_INDEX));
312        }
313    }
314
315    Alarm(Parcel p) {
316        id = p.readLong();
317        enabled = p.readInt() == 1;
318        hour = p.readInt();
319        minutes = p.readInt();
320        daysOfWeek = Weekdays.fromBits(p.readInt());
321        vibrate = p.readInt() == 1;
322        label = p.readString();
323        alert = p.readParcelable(null);
324        deleteAfterUse = p.readInt() == 1;
325    }
326
327    /**
328     * @return the deeplink that identifies this alarm
329     */
330    public Uri getContentUri() {
331        return getContentUri(id);
332    }
333
334    public String getLabelOrDefault(Context context) {
335        return label.isEmpty() ? context.getString(R.string.default_label) : label;
336    }
337
338    /**
339     * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
340     * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
341     */
342    public boolean canPreemptivelyDismiss() {
343        return instanceState == AlarmInstance.SNOOZE_STATE
344                || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
345                || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
346                || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
347    }
348
349    public void writeToParcel(Parcel p, int flags) {
350        p.writeLong(id);
351        p.writeInt(enabled ? 1 : 0);
352        p.writeInt(hour);
353        p.writeInt(minutes);
354        p.writeInt(daysOfWeek.getBits());
355        p.writeInt(vibrate ? 1 : 0);
356        p.writeString(label);
357        p.writeParcelable(alert, flags);
358        p.writeInt(deleteAfterUse ? 1 : 0);
359    }
360
361    public int describeContents() {
362        return 0;
363    }
364
365    public AlarmInstance createInstanceAfter(Calendar time) {
366        Calendar nextInstanceTime = getNextAlarmTime(time);
367        AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
368        result.mVibrate = vibrate;
369        result.mLabel = label;
370        result.mRingtone = alert;
371        return result;
372    }
373
374    /**
375     *
376     * @param currentTime the current time
377     * @return previous firing time, or null if this is a one-time alarm.
378     */
379    public Calendar getPreviousAlarmTime(Calendar currentTime) {
380        final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
381        previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
382        previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
383        previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
384        previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
385        previousInstanceTime.set(Calendar.MINUTE, minutes);
386        previousInstanceTime.set(Calendar.SECOND, 0);
387        previousInstanceTime.set(Calendar.MILLISECOND, 0);
388
389        final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
390        if (subtractDays > 0) {
391            previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
392            return previousInstanceTime;
393        } else {
394            return null;
395        }
396    }
397
398    public Calendar getNextAlarmTime(Calendar currentTime) {
399        final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
400        nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
401        nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
402        nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
403        nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
404        nextInstanceTime.set(Calendar.MINUTE, minutes);
405        nextInstanceTime.set(Calendar.SECOND, 0);
406        nextInstanceTime.set(Calendar.MILLISECOND, 0);
407
408        // If we are still behind the passed in currentTime, then add a day
409        if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
410            nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
411        }
412
413        // The day of the week might be invalid, so find next valid one
414        final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
415        if (addDays > 0) {
416            nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
417        }
418
419        // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
420        // Reset the desired hour and minute now that the correct day has been chosen.
421        nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
422        nextInstanceTime.set(Calendar.MINUTE, minutes);
423
424        return nextInstanceTime;
425    }
426
427    @Override
428    public boolean equals(Object o) {
429        if (!(o instanceof Alarm)) return false;
430        final Alarm other = (Alarm) o;
431        return id == other.id;
432    }
433
434    @Override
435    public int hashCode() {
436        return Long.valueOf(id).hashCode();
437    }
438
439    @Override
440    public String toString() {
441        return "Alarm{" +
442                "alert=" + alert +
443                ", id=" + id +
444                ", enabled=" + enabled +
445                ", hour=" + hour +
446                ", minutes=" + minutes +
447                ", daysOfWeek=" + daysOfWeek +
448                ", vibrate=" + vibrate +
449                ", label='" + label + '\'' +
450                ", deleteAfterUse=" + deleteAfterUse +
451                '}';
452    }
453}