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.Intent;
24 import android.database.Cursor;
25 import android.media.RingtoneManager;
26 import android.net.Uri;
27 
28 import com.android.deskclock.LogUtils;
29 import com.android.deskclock.R;
30 import com.android.deskclock.alarms.AlarmStateManager;
31 import com.android.deskclock.data.DataModel;
32 
33 import java.util.Calendar;
34 import java.util.LinkedList;
35 import java.util.List;
36 
37 public final class AlarmInstance implements ClockContract.InstancesColumns {
38     /**
39      * Offset from alarm time to show low priority notification
40      */
41     public static final int LOW_NOTIFICATION_HOUR_OFFSET = -2;
42 
43     /**
44      * Offset from alarm time to show high priority notification
45      */
46     public static final int HIGH_NOTIFICATION_MINUTE_OFFSET = -30;
47 
48     /**
49      * Offset from alarm time to stop showing missed notification.
50      */
51     private static final int MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12;
52 
53     /**
54      * AlarmInstances start with an invalid id when it hasn't been saved to the database.
55      */
56     public static final long INVALID_ID = -1;
57 
58     private static final String[] QUERY_COLUMNS = {
59             _ID,
60             YEAR,
61             MONTH,
62             DAY,
63             HOUR,
64             MINUTES,
65             LABEL,
66             VIBRATE,
67             RINGTONE,
68             ALARM_ID,
69             ALARM_STATE
70     };
71 
72     /**
73      * These save calls to cursor.getColumnIndexOrThrow()
74      * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
75      */
76     private static final int ID_INDEX = 0;
77     private static final int YEAR_INDEX = 1;
78     private static final int MONTH_INDEX = 2;
79     private static final int DAY_INDEX = 3;
80     private static final int HOUR_INDEX = 4;
81     private static final int MINUTES_INDEX = 5;
82     private static final int LABEL_INDEX = 6;
83     private static final int VIBRATE_INDEX = 7;
84     private static final int RINGTONE_INDEX = 8;
85     private static final int ALARM_ID_INDEX = 9;
86     private static final int ALARM_STATE_INDEX = 10;
87 
88     private static final int COLUMN_COUNT = ALARM_STATE_INDEX + 1;
89 
createContentValues(AlarmInstance instance)90     public static ContentValues createContentValues(AlarmInstance instance) {
91         ContentValues values = new ContentValues(COLUMN_COUNT);
92         if (instance.mId != INVALID_ID) {
93             values.put(_ID, instance.mId);
94         }
95 
96         values.put(YEAR, instance.mYear);
97         values.put(MONTH, instance.mMonth);
98         values.put(DAY, instance.mDay);
99         values.put(HOUR, instance.mHour);
100         values.put(MINUTES, instance.mMinute);
101         values.put(LABEL, instance.mLabel);
102         values.put(VIBRATE, instance.mVibrate ? 1 : 0);
103         if (instance.mRingtone == null) {
104             // We want to put null in the database, so we'll be able
105             // to pick up on changes to the default alarm
106             values.putNull(RINGTONE);
107         } else {
108             values.put(RINGTONE, instance.mRingtone.toString());
109         }
110         values.put(ALARM_ID, instance.mAlarmId);
111         values.put(ALARM_STATE, instance.mAlarmState);
112         return values;
113     }
114 
createIntent(String action, long instanceId)115     public static Intent createIntent(String action, long instanceId) {
116         return new Intent(action).setData(getContentUri(instanceId));
117     }
118 
createIntent(Context context, Class<?> cls, long instanceId)119     public static Intent createIntent(Context context, Class<?> cls, long instanceId) {
120         return new Intent(context, cls).setData(getContentUri(instanceId));
121     }
122 
getId(Uri contentUri)123     public static long getId(Uri contentUri) {
124         return ContentUris.parseId(contentUri);
125     }
126 
127     /**
128      * @return the {@link Uri} identifying the alarm instance
129      */
getContentUri(long instanceId)130     public static Uri getContentUri(long instanceId) {
131         return ContentUris.withAppendedId(CONTENT_URI, instanceId);
132     }
133 
134     /**
135      * Get alarm instance from instanceId.
136      *
137      * @param cr provides access to the content model
138      * @param instanceId for the desired instance.
139      * @return instance if found, null otherwise
140      */
getInstance(ContentResolver cr, long instanceId)141     public static AlarmInstance getInstance(ContentResolver cr, long instanceId) {
142         try (Cursor cursor = cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)) {
143             if (cursor != null && cursor.moveToFirst()) {
144                 return new AlarmInstance(cursor, false /* joinedTable */);
145             }
146         }
147 
148         return null;
149     }
150 
151     /**
152      * Get alarm instance for the {@code contentUri}.
153      *
154      * @param cr provides access to the content model
155      * @param contentUri the {@link #getContentUri deeplink} for the desired instance
156      * @return instance if found, null otherwise
157      */
getInstance(ContentResolver cr, Uri contentUri)158     public static AlarmInstance getInstance(ContentResolver cr, Uri contentUri) {
159         final long instanceId = ContentUris.parseId(contentUri);
160         return getInstance(cr, instanceId);
161     }
162 
163     /**
164      * Get an alarm instances by alarmId.
165      *
166      * @param contentResolver provides access to the content model
167      * @param alarmId of instances desired.
168      * @return list of alarms instances that are owned by alarmId.
169      */
getInstancesByAlarmId(ContentResolver contentResolver, long alarmId)170     public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver,
171             long alarmId) {
172         return getInstances(contentResolver, ALARM_ID + "=" + alarmId);
173     }
174 
175     /**
176      * Get the next instance of an alarm given its alarmId
177      * @param contentResolver provides access to the content model
178      * @param alarmId of instance desired
179      * @return the next instance of an alarm by alarmId.
180      */
getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver, long alarmId)181     public static AlarmInstance getNextUpcomingInstanceByAlarmId(ContentResolver contentResolver,
182                                                                  long alarmId) {
183         final List<AlarmInstance> alarmInstances = getInstancesByAlarmId(contentResolver, alarmId);
184         if (alarmInstances.isEmpty()) {
185             return null;
186         }
187         AlarmInstance nextAlarmInstance = alarmInstances.get(0);
188         for (AlarmInstance instance : alarmInstances) {
189             if (instance.getAlarmTime().before(nextAlarmInstance.getAlarmTime())) {
190                 nextAlarmInstance = instance;
191             }
192         }
193         return nextAlarmInstance;
194     }
195 
196     /**
197      * Get alarm instance by id and state.
198      */
getInstancesByInstanceIdAndState( ContentResolver contentResolver, long alarmInstanceId, int state)199     public static List<AlarmInstance> getInstancesByInstanceIdAndState(
200             ContentResolver contentResolver, long alarmInstanceId, int state) {
201         return getInstances(contentResolver, _ID + "=" + alarmInstanceId + " AND " + ALARM_STATE +
202                 "=" + state);
203     }
204 
205     /**
206      * Get alarm instances in the specified state.
207      */
getInstancesByState( ContentResolver contentResolver, int state)208     public static List<AlarmInstance> getInstancesByState(
209             ContentResolver contentResolver, int state) {
210         return getInstances(contentResolver, ALARM_STATE + "=" + state);
211     }
212 
213     /**
214      * Get a list of instances given selection.
215      *
216      * @param cr provides access to the content model
217      * @param selection A filter declaring which rows to return, formatted as an
218      *         SQL WHERE clause (excluding the WHERE itself). Passing null will
219      *         return all rows for the given URI.
220      * @param selectionArgs You may include ?s in selection, which will be
221      *         replaced by the values from selectionArgs, in the order that they
222      *         appear in the selection. The values will be bound as Strings.
223      * @return list of alarms matching where clause or empty list if none found.
224      */
getInstances(ContentResolver cr, String selection, String... selectionArgs)225     public static List<AlarmInstance> getInstances(ContentResolver cr, String selection,
226                                                    String... selectionArgs) {
227         final List<AlarmInstance> result = new LinkedList<>();
228         try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
229             if (cursor != null && cursor.moveToFirst()) {
230                 do {
231                     result.add(new AlarmInstance(cursor, false /* joinedTable */));
232                 } while (cursor.moveToNext());
233             }
234         }
235 
236         return result;
237     }
238 
addInstance(ContentResolver contentResolver, AlarmInstance instance)239     public static AlarmInstance addInstance(ContentResolver contentResolver,
240             AlarmInstance instance) {
241         // Make sure we are not adding a duplicate instances. This is not a
242         // fix and should never happen. This is only a safe guard against bad code, and you
243         // should fix the root issue if you see the error message.
244         String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId;
245         for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) {
246             if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) {
247                 LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to "
248                         + instance);
249                 // Copy over the new instance values and update the db
250                 instance.mId = otherInstances.mId;
251                 updateInstance(contentResolver, instance);
252                 return instance;
253             }
254         }
255 
256         ContentValues values = createContentValues(instance);
257         Uri uri = contentResolver.insert(CONTENT_URI, values);
258         instance.mId = getId(uri);
259         return instance;
260     }
261 
updateInstance(ContentResolver contentResolver, AlarmInstance instance)262     public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) {
263         if (instance.mId == INVALID_ID) return false;
264         ContentValues values = createContentValues(instance);
265         long rowsUpdated = contentResolver.update(getContentUri(instance.mId), values, null, null);
266         return rowsUpdated == 1;
267     }
268 
deleteInstance(ContentResolver contentResolver, long instanceId)269     public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) {
270         if (instanceId == INVALID_ID) return false;
271         int deletedRows = contentResolver.delete(getContentUri(instanceId), "", null);
272         return deletedRows == 1;
273     }
274 
deleteOtherInstances(Context context, ContentResolver contentResolver, long alarmId, long instanceId)275     public static void deleteOtherInstances(Context context, ContentResolver contentResolver,
276             long alarmId, long instanceId) {
277         final List<AlarmInstance> instances = getInstancesByAlarmId(contentResolver, alarmId);
278         for (AlarmInstance instance : instances) {
279             if (instance.mId != instanceId) {
280                 AlarmStateManager.unregisterInstance(context, instance);
281                 deleteInstance(contentResolver, instance.mId);
282             }
283         }
284     }
285 
286     // Public fields
287     public long mId;
288     public int mYear;
289     public int mMonth;
290     public int mDay;
291     public int mHour;
292     public int mMinute;
293     public String mLabel;
294     public boolean mVibrate;
295     public Uri mRingtone;
296     public Long mAlarmId;
297     public int mAlarmState;
298 
AlarmInstance(Calendar calendar, Long alarmId)299     public AlarmInstance(Calendar calendar, Long alarmId) {
300         this(calendar);
301         mAlarmId = alarmId;
302     }
303 
AlarmInstance(Calendar calendar)304     public AlarmInstance(Calendar calendar) {
305         mId = INVALID_ID;
306         setAlarmTime(calendar);
307         mLabel = "";
308         mVibrate = false;
309         mRingtone = null;
310         mAlarmState = SILENT_STATE;
311     }
312 
AlarmInstance(AlarmInstance instance)313     public AlarmInstance(AlarmInstance instance) {
314          this.mId = instance.mId;
315          this.mYear = instance.mYear;
316          this.mMonth = instance.mMonth;
317          this.mDay = instance.mDay;
318          this.mHour = instance.mHour;
319          this.mMinute = instance.mMinute;
320          this.mLabel = instance.mLabel;
321          this.mVibrate = instance.mVibrate;
322          this.mRingtone = instance.mRingtone;
323          this.mAlarmId = instance.mAlarmId;
324          this.mAlarmState = instance.mAlarmState;
325     }
326 
AlarmInstance(Cursor c, boolean joinedTable)327     public AlarmInstance(Cursor c, boolean joinedTable) {
328         if (joinedTable) {
329             mId = c.getLong(Alarm.INSTANCE_ID_INDEX);
330             mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX);
331             mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX);
332             mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX);
333             mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX);
334             mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX);
335             mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX);
336             mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1;
337         } else {
338             mId = c.getLong(ID_INDEX);
339             mYear = c.getInt(YEAR_INDEX);
340             mMonth = c.getInt(MONTH_INDEX);
341             mDay = c.getInt(DAY_INDEX);
342             mHour = c.getInt(HOUR_INDEX);
343             mMinute = c.getInt(MINUTES_INDEX);
344             mLabel = c.getString(LABEL_INDEX);
345             mVibrate = c.getInt(VIBRATE_INDEX) == 1;
346         }
347         if (c.isNull(RINGTONE_INDEX)) {
348             // Should we be saving this with the current ringtone or leave it null
349             // so it changes when user changes default ringtone?
350             mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
351         } else {
352             mRingtone = Uri.parse(c.getString(RINGTONE_INDEX));
353         }
354 
355         if (!c.isNull(ALARM_ID_INDEX)) {
356             mAlarmId = c.getLong(ALARM_ID_INDEX);
357         }
358         mAlarmState = c.getInt(ALARM_STATE_INDEX);
359     }
360 
361     /**
362      * @return the deeplink that identifies this alarm instance
363      */
getContentUri()364     public Uri getContentUri() {
365         return getContentUri(mId);
366     }
367 
getLabelOrDefault(Context context)368     public String getLabelOrDefault(Context context) {
369         return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel;
370     }
371 
setAlarmTime(Calendar calendar)372     public void setAlarmTime(Calendar calendar) {
373         mYear = calendar.get(Calendar.YEAR);
374         mMonth = calendar.get(Calendar.MONTH);
375         mDay = calendar.get(Calendar.DAY_OF_MONTH);
376         mHour = calendar.get(Calendar.HOUR_OF_DAY);
377         mMinute = calendar.get(Calendar.MINUTE);
378     }
379 
380     /**
381      * Return the time when a alarm should fire.
382      *
383      * @return the time
384      */
getAlarmTime()385     public Calendar getAlarmTime() {
386         Calendar calendar = Calendar.getInstance();
387         calendar.set(Calendar.YEAR, mYear);
388         calendar.set(Calendar.MONTH, mMonth);
389         calendar.set(Calendar.DAY_OF_MONTH, mDay);
390         calendar.set(Calendar.HOUR_OF_DAY, mHour);
391         calendar.set(Calendar.MINUTE, mMinute);
392         calendar.set(Calendar.SECOND, 0);
393         calendar.set(Calendar.MILLISECOND, 0);
394         return calendar;
395     }
396 
397     /**
398      * Return the time when a low priority notification should be shown.
399      *
400      * @return the time
401      */
getLowNotificationTime()402     public Calendar getLowNotificationTime() {
403         Calendar calendar = getAlarmTime();
404         calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);
405         return calendar;
406     }
407 
408     /**
409      * Return the time when a high priority notification should be shown.
410      *
411      * @return the time
412      */
getHighNotificationTime()413     public Calendar getHighNotificationTime() {
414         Calendar calendar = getAlarmTime();
415         calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET);
416         return calendar;
417     }
418 
419     /**
420      * Return the time when a missed notification should be removed.
421      *
422      * @return the time
423      */
getMissedTimeToLive()424     public Calendar getMissedTimeToLive() {
425         Calendar calendar = getAlarmTime();
426         calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET);
427         return calendar;
428     }
429 
430     /**
431      * Return the time when the alarm should stop firing and be marked as missed.
432      *
433      * @return the time when alarm should be silence, or null if never
434      */
getTimeout()435     public Calendar getTimeout() {
436         final int timeoutMinutes = DataModel.getDataModel().getAlarmTimeout();
437 
438         // Alarm silence has been set to "None"
439         if (timeoutMinutes < 0) {
440             return null;
441         }
442 
443         Calendar calendar = getAlarmTime();
444         calendar.add(Calendar.MINUTE, timeoutMinutes);
445         return calendar;
446     }
447 
448     @Override
equals(Object o)449     public boolean equals(Object o) {
450         if (!(o instanceof AlarmInstance)) return false;
451         final AlarmInstance other = (AlarmInstance) o;
452         return mId == other.mId;
453     }
454 
455     @Override
hashCode()456     public int hashCode() {
457         return Long.valueOf(mId).hashCode();
458     }
459 
460     @Override
toString()461     public String toString() {
462         return "AlarmInstance{" +
463                 "mId=" + mId +
464                 ", mYear=" + mYear +
465                 ", mMonth=" + mMonth +
466                 ", mDay=" + mDay +
467                 ", mHour=" + mHour +
468                 ", mMinute=" + mMinute +
469                 ", mLabel=" + mLabel +
470                 ", mVibrate=" + mVibrate +
471                 ", mRingtone=" + mRingtone +
472                 ", mAlarmId=" + mAlarmId +
473                 ", mAlarmState=" + mAlarmState +
474                 '}';
475     }
476 }
477