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