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