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 import com.android.deskclock.data.Weekdays;
34 
35 import java.util.Calendar;
36 import java.util.LinkedList;
37 import java.util.List;
38 
39 public 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 
createContentValues(Alarm alarm)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 
createIntent(Context context, Class<?> cls, long alarmId)136     public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
137         return new Intent(context, cls).setData(getContentUri(alarmId));
138     }
139 
getContentUri(long alarmId)140     public static Uri getContentUri(long alarmId) {
141         return ContentUris.withAppendedId(CONTENT_URI, alarmId);
142     }
143 
getId(Uri contentUri)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      */
getAlarmsCursorLoader(Context context)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 void onContentChanged() {
159                 // There is a bug in Loader which can result in stale data if a loader is stopped
160                 // immediately after a call to onContentChanged. As a workaround we stop the
161                 // loader before delivering onContentChanged to ensure mContentChanged is set to
162                 // true before forceLoad is called.
163                 if (isStarted() && !isAbandoned()) {
164                     stopLoading();
165                     super.onContentChanged();
166                     startLoading();
167                 } else {
168                     super.onContentChanged();
169                 }
170             }
171 
172             @Override
173             public Cursor loadInBackground() {
174                 // Prime the ringtone title cache for later access. Most alarms will refer to
175                 // system ringtones.
176                 DataModel.getDataModel().loadRingtoneTitles();
177 
178                 return super.loadInBackground();
179             }
180         };
181     }
182 
183     /**
184      * Get alarm by id.
185      *
186      * @param cr provides access to the content model
187      * @param alarmId for the desired alarm.
188      * @return alarm if found, null otherwise
189      */
190     public static Alarm getAlarm(ContentResolver cr, long alarmId) {
191         try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) {
192             if (cursor.moveToFirst()) {
193                 return new Alarm(cursor);
194             }
195         }
196 
197         return null;
198     }
199     /**
200      * Get alarm for the {@code contentUri}.
201      *
202      * @param cr provides access to the content model
203      * @param contentUri the {@link #getContentUri deeplink} for the desired alarm
204      * @return instance if found, null otherwise
205      */
206     public static Alarm getAlarm(ContentResolver cr, Uri contentUri) {
207         return getAlarm(cr, ContentUris.parseId(contentUri));
208     }
209 
210     /**
211      * Get all alarms given conditions.
212      *
213      * @param cr provides access to the content model
214      * @param selection A filter declaring which rows to return, formatted as an
215      *         SQL WHERE clause (excluding the WHERE itself). Passing null will
216      *         return all rows for the given URI.
217      * @param selectionArgs You may include ?s in selection, which will be
218      *         replaced by the values from selectionArgs, in the order that they
219      *         appear in the selection. The values will be bound as Strings.
220      * @return list of alarms matching where clause or empty list if none found.
221      */
222     public static List<Alarm> getAlarms(ContentResolver cr, String selection,
223             String... selectionArgs) {
224         final List<Alarm> result = new LinkedList<>();
225         try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
226             if (cursor != null && cursor.moveToFirst()) {
227                 do {
228                     result.add(new Alarm(cursor));
229                 } while (cursor.moveToNext());
230             }
231         }
232 
233         return result;
234     }
235 
236     public static boolean isTomorrow(Alarm alarm, Calendar now) {
237         if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) {
238             return false;
239         }
240 
241         final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes;
242         final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE);
243         return totalAlarmMinutes <= totalNowMinutes;
244     }
245 
246     public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
247         ContentValues values = createContentValues(alarm);
248         Uri uri = contentResolver.insert(CONTENT_URI, values);
249         alarm.id = getId(uri);
250         return alarm;
251     }
252 
253     public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
254         if (alarm.id == Alarm.INVALID_ID) return false;
255         ContentValues values = createContentValues(alarm);
256         long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null);
257         return rowsUpdated == 1;
258     }
259 
260     public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
261         if (alarmId == INVALID_ID) return false;
262         int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null);
263         return deletedRows == 1;
264     }
265 
266     public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
267         public Alarm createFromParcel(Parcel p) {
268             return new Alarm(p);
269         }
270 
271         public Alarm[] newArray(int size) {
272             return new Alarm[size];
273         }
274     };
275 
276     // Public fields
277     // TODO: Refactor instance names
278     public long id;
279     public boolean enabled;
280     public int hour;
281     public int minutes;
282     public Weekdays daysOfWeek;
283     public boolean vibrate;
284     public String label;
285     public Uri alert;
286     public boolean deleteAfterUse;
287     public int instanceState;
288     public int instanceId;
289 
290     // Creates a default alarm at the current time.
291     public Alarm() {
292         this(0, 0);
293     }
294 
295     public Alarm(int hour, int minutes) {
296         this.id = INVALID_ID;
297         this.hour = hour;
298         this.minutes = minutes;
299         this.vibrate = true;
300         this.daysOfWeek = Weekdays.NONE;
301         this.label = "";
302         this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
303         this.deleteAfterUse = false;
304     }
305 
306     public Alarm(Cursor c) {
307         id = c.getLong(ID_INDEX);
308         enabled = c.getInt(ENABLED_INDEX) == 1;
309         hour = c.getInt(HOUR_INDEX);
310         minutes = c.getInt(MINUTES_INDEX);
311         daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX));
312         vibrate = c.getInt(VIBRATE_INDEX) == 1;
313         label = c.getString(LABEL_INDEX);
314         deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
315 
316         if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
317             instanceState = c.getInt(INSTANCE_STATE_INDEX);
318             instanceId = c.getInt(INSTANCE_ID_INDEX);
319         }
320 
321         if (c.isNull(RINGTONE_INDEX)) {
322             // Should we be saving this with the current ringtone or leave it null
323             // so it changes when user changes default ringtone?
324             alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
325         } else {
326             alert = Uri.parse(c.getString(RINGTONE_INDEX));
327         }
328     }
329 
330     Alarm(Parcel p) {
331         id = p.readLong();
332         enabled = p.readInt() == 1;
333         hour = p.readInt();
334         minutes = p.readInt();
335         daysOfWeek = Weekdays.fromBits(p.readInt());
336         vibrate = p.readInt() == 1;
337         label = p.readString();
338         alert = p.readParcelable(null);
339         deleteAfterUse = p.readInt() == 1;
340     }
341 
342     /**
343      * @return the deeplink that identifies this alarm
344      */
345     public Uri getContentUri() {
346         return getContentUri(id);
347     }
348 
349     public String getLabelOrDefault(Context context) {
350         return label.isEmpty() ? context.getString(R.string.default_label) : label;
351     }
352 
353     /**
354      * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
355      * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
356      */
357     public boolean canPreemptivelyDismiss() {
358         return instanceState == AlarmInstance.SNOOZE_STATE
359                 || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
360                 || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
361                 || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
362     }
363 
364     public void writeToParcel(Parcel p, int flags) {
365         p.writeLong(id);
366         p.writeInt(enabled ? 1 : 0);
367         p.writeInt(hour);
368         p.writeInt(minutes);
369         p.writeInt(daysOfWeek.getBits());
370         p.writeInt(vibrate ? 1 : 0);
371         p.writeString(label);
372         p.writeParcelable(alert, flags);
373         p.writeInt(deleteAfterUse ? 1 : 0);
374     }
375 
376     public int describeContents() {
377         return 0;
378     }
379 
380     public AlarmInstance createInstanceAfter(Calendar time) {
381         Calendar nextInstanceTime = getNextAlarmTime(time);
382         AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
383         result.mVibrate = vibrate;
384         result.mLabel = label;
385         result.mRingtone = alert;
386         return result;
387     }
388 
389     /**
390      *
391      * @param currentTime the current time
392      * @return previous firing time, or null if this is a one-time alarm.
393      */
394     public Calendar getPreviousAlarmTime(Calendar currentTime) {
395         final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
396         previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
397         previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
398         previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
399         previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
400         previousInstanceTime.set(Calendar.MINUTE, minutes);
401         previousInstanceTime.set(Calendar.SECOND, 0);
402         previousInstanceTime.set(Calendar.MILLISECOND, 0);
403 
404         final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime);
405         if (subtractDays > 0) {
406             previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
407             return previousInstanceTime;
408         } else {
409             return null;
410         }
411     }
412 
413     public Calendar getNextAlarmTime(Calendar currentTime) {
414         final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone());
415         nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
416         nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
417         nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
418         nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
419         nextInstanceTime.set(Calendar.MINUTE, minutes);
420         nextInstanceTime.set(Calendar.SECOND, 0);
421         nextInstanceTime.set(Calendar.MILLISECOND, 0);
422 
423         // If we are still behind the passed in currentTime, then add a day
424         if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
425             nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
426         }
427 
428         // The day of the week might be invalid, so find next valid one
429         final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime);
430         if (addDays > 0) {
431             nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
432         }
433 
434         // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
435         // Reset the desired hour and minute now that the correct day has been chosen.
436         nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
437         nextInstanceTime.set(Calendar.MINUTE, minutes);
438 
439         return nextInstanceTime;
440     }
441 
442     @Override
443     public boolean equals(Object o) {
444         if (!(o instanceof Alarm)) return false;
445         final Alarm other = (Alarm) o;
446         return id == other.id;
447     }
448 
449     @Override
450     public int hashCode() {
451         return Long.valueOf(id).hashCode();
452     }
453 
454     @Override
455     public String toString() {
456         return "Alarm{" +
457                 "alert=" + alert +
458                 ", id=" + id +
459                 ", enabled=" + enabled +
460                 ", hour=" + hour +
461                 ", minutes=" + minutes +
462                 ", daysOfWeek=" + daysOfWeek +
463                 ", vibrate=" + vibrate +
464                 ", label='" + label + '\'' +
465                 ", deleteAfterUse=" + deleteAfterUse +
466                 '}';
467     }
468 }
469