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