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.SettingsActivity;
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      * Default timeout for alarms in minutes.
55      */
56     private static final String DEFAULT_ALARM_TIMEOUT_SETTING = "10";
57 
58     /**
59      * AlarmInstances start with an invalid id when it hasn't been saved to the database.
60      */
61     public static final long INVALID_ID = -1;
62 
63     private static final String[] QUERY_COLUMNS = {
64             _ID,
65             YEAR,
66             MONTH,
67             DAY,
68             HOUR,
69             MINUTES,
70             LABEL,
71             VIBRATE,
72             RINGTONE,
73             ALARM_ID,
74             ALARM_STATE
75     };
76 
77     /**
78      * These save calls to cursor.getColumnIndexOrThrow()
79      * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
80      */
81     private static final int ID_INDEX = 0;
82     private static final int YEAR_INDEX = 1;
83     private static final int MONTH_INDEX = 2;
84     private static final int DAY_INDEX = 3;
85     private static final int HOUR_INDEX = 4;
86     private static final int MINUTES_INDEX = 5;
87     private static final int LABEL_INDEX = 6;
88     private static final int VIBRATE_INDEX = 7;
89     private static final int RINGTONE_INDEX = 8;
90     private static final int ALARM_ID_INDEX = 9;
91     private static final int ALARM_STATE_INDEX = 10;
92 
93     private static final int COLUMN_COUNT = ALARM_STATE_INDEX + 1;
94     private Calendar mTimeout;
95 
createContentValues(AlarmInstance instance)96     public static ContentValues createContentValues(AlarmInstance instance) {
97         ContentValues values = new ContentValues(COLUMN_COUNT);
98         if (instance.mId != INVALID_ID) {
99             values.put(_ID, instance.mId);
100         }
101 
102         values.put(YEAR, instance.mYear);
103         values.put(MONTH, instance.mMonth);
104         values.put(DAY, instance.mDay);
105         values.put(HOUR, instance.mHour);
106         values.put(MINUTES, instance.mMinute);
107         values.put(LABEL, instance.mLabel);
108         values.put(VIBRATE, instance.mVibrate ? 1 : 0);
109         if (instance.mRingtone == null) {
110             // We want to put null in the database, so we'll be able
111             // to pick up on changes to the default alarm
112             values.putNull(RINGTONE);
113         } else {
114             values.put(RINGTONE, instance.mRingtone.toString());
115         }
116         values.put(ALARM_ID, instance.mAlarmId);
117         values.put(ALARM_STATE, instance.mAlarmState);
118         return values;
119     }
120 
createIntent(String action, long instanceId)121     public static Intent createIntent(String action, long instanceId) {
122         return new Intent(action).setData(getUri(instanceId));
123     }
124 
createIntent(Context context, Class<?> cls, long instanceId)125     public static Intent createIntent(Context context, Class<?> cls, long instanceId) {
126         return new Intent(context, cls).setData(getUri(instanceId));
127     }
128 
getId(Uri contentUri)129     public static long getId(Uri contentUri) {
130         return ContentUris.parseId(contentUri);
131     }
132 
getUri(long instanceId)133     public static Uri getUri(long instanceId) {
134         return ContentUris.withAppendedId(CONTENT_URI, instanceId);
135     }
136 
137     /**
138      * Get alarm instance from instanceId.
139      *
140      * @param contentResolver to perform the query on.
141      * @param instanceId for the desired instance.
142      * @return instance if found, null otherwise
143      */
getInstance(ContentResolver contentResolver, long instanceId)144     public static AlarmInstance getInstance(ContentResolver contentResolver, long instanceId) {
145         Cursor cursor = contentResolver.query(getUri(instanceId), QUERY_COLUMNS, null, null, null);
146         AlarmInstance result = null;
147         if (cursor == null) {
148             return result;
149         }
150 
151         try {
152             if (cursor.moveToFirst()) {
153                 result = new AlarmInstance(cursor);
154             }
155         } finally {
156             cursor.close();
157         }
158 
159         return result;
160     }
161 
162     /**
163      * Get an alarm instances by alarmId.
164      *
165      * @param contentResolver to perform the query on.
166      * @param alarmId of instances desired.
167      * @return list of alarms instances that are owned by alarmId.
168      */
getInstancesByAlarmId(ContentResolver contentResolver, long alarmId)169     public static List<AlarmInstance> getInstancesByAlarmId(ContentResolver contentResolver,
170             long alarmId) {
171         return getInstances(contentResolver, ALARM_ID + "=" + alarmId);
172     }
173 
174     /**
175      * Get a list of instances given selection.
176      *
177      * @param contentResolver 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      */
getInstances(ContentResolver contentResolver, String selection, String ... selectionArgs)186     public static List<AlarmInstance> getInstances(ContentResolver contentResolver,
187             String selection, String ... selectionArgs) {
188         Cursor cursor  = contentResolver.query(CONTENT_URI, QUERY_COLUMNS,
189                 selection, selectionArgs, null);
190         List<AlarmInstance> result = new LinkedList<AlarmInstance>();
191         if (cursor == null) {
192             return result;
193         }
194 
195         try {
196             if (cursor.moveToFirst()) {
197                 do {
198                     result.add(new AlarmInstance(cursor));
199                 } while (cursor.moveToNext());
200             }
201         } finally {
202             cursor.close();
203         }
204 
205         return result;
206     }
207 
addInstance(ContentResolver contentResolver, AlarmInstance instance)208     public static AlarmInstance addInstance(ContentResolver contentResolver,
209             AlarmInstance instance) {
210         // Make sure we are not adding a duplicate instances. This is not a
211         // fix and should never happen. This is only a safe guard against bad code, and you
212         // should fix the root issue if you see the error message.
213         String dupSelector = AlarmInstance.ALARM_ID + " = " + instance.mAlarmId;
214         for (AlarmInstance otherInstances : getInstances(contentResolver, dupSelector)) {
215             if (otherInstances.getAlarmTime().equals(instance.getAlarmTime())) {
216                 LogUtils.i("Detected duplicate instance in DB. Updating " + otherInstances + " to "
217                         + instance);
218                 // Copy over the new instance values and update the db
219                 instance.mId = otherInstances.mId;
220                 updateInstance(contentResolver, instance);
221                 return instance;
222             }
223         }
224 
225         ContentValues values = createContentValues(instance);
226         Uri uri = contentResolver.insert(CONTENT_URI, values);
227         instance.mId = getId(uri);
228         return instance;
229     }
230 
updateInstance(ContentResolver contentResolver, AlarmInstance instance)231     public static boolean updateInstance(ContentResolver contentResolver, AlarmInstance instance) {
232         if (instance.mId == INVALID_ID) return false;
233         ContentValues values = createContentValues(instance);
234         long rowsUpdated = contentResolver.update(getUri(instance.mId), values, null, null);
235         return rowsUpdated == 1;
236     }
237 
deleteInstance(ContentResolver contentResolver, long instanceId)238     public static boolean deleteInstance(ContentResolver contentResolver, long instanceId) {
239         if (instanceId == INVALID_ID) return false;
240         int deletedRows = contentResolver.delete(getUri(instanceId), "", null);
241         return deletedRows == 1;
242     }
243 
244     // Public fields
245     public long mId;
246     public int mYear;
247     public int mMonth;
248     public int mDay;
249     public int mHour;
250     public int mMinute;
251     public String mLabel;
252     public boolean mVibrate;
253     public Uri mRingtone;
254     public Long mAlarmId;
255     public int mAlarmState;
256 
AlarmInstance(Calendar calendar, Long alarmId)257     public AlarmInstance(Calendar calendar, Long alarmId) {
258         this(calendar);
259         mAlarmId = alarmId;
260     }
261 
AlarmInstance(Calendar calendar)262     public AlarmInstance(Calendar calendar) {
263         mId = INVALID_ID;
264         setAlarmTime(calendar);
265         mLabel = "";
266         mVibrate = false;
267         mRingtone = null;
268         mAlarmState = SILENT_STATE;
269     }
270 
AlarmInstance(Cursor c)271     public AlarmInstance(Cursor c) {
272         mId = c.getLong(ID_INDEX);
273         mYear = c.getInt(YEAR_INDEX);
274         mMonth = c.getInt(MONTH_INDEX);
275         mDay = c.getInt(DAY_INDEX);
276         mHour = c.getInt(HOUR_INDEX);
277         mMinute = c.getInt(MINUTES_INDEX);
278         mLabel = c.getString(LABEL_INDEX);
279         mVibrate = c.getInt(VIBRATE_INDEX) == 1;
280         if (c.isNull(RINGTONE_INDEX)) {
281             // Should we be saving this with the current ringtone or leave it null
282             // so it changes when user changes default ringtone?
283             mRingtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
284         } else {
285             mRingtone = Uri.parse(c.getString(RINGTONE_INDEX));
286         }
287 
288         if (!c.isNull(ALARM_ID_INDEX)) {
289             mAlarmId = c.getLong(ALARM_ID_INDEX);
290         }
291         mAlarmState = c.getInt(ALARM_STATE_INDEX);
292     }
293 
getLabelOrDefault(Context context)294     public String getLabelOrDefault(Context context) {
295         return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel;
296     }
297 
setAlarmTime(Calendar calendar)298     public void setAlarmTime(Calendar calendar) {
299         mYear = calendar.get(Calendar.YEAR);
300         mMonth = calendar.get(Calendar.MONTH);
301         mDay = calendar.get(Calendar.DAY_OF_MONTH);
302         mHour = calendar.get(Calendar.HOUR_OF_DAY);
303         mMinute = calendar.get(Calendar.MINUTE);
304     }
305 
306     /**
307      * Return the time when a alarm should fire.
308      *
309      * @return the time
310      */
getAlarmTime()311     public Calendar getAlarmTime() {
312         Calendar calendar = Calendar.getInstance();
313         calendar.set(Calendar.YEAR, mYear);
314         calendar.set(Calendar.MONTH, mMonth);
315         calendar.set(Calendar.DAY_OF_MONTH, mDay);
316         calendar.set(Calendar.HOUR_OF_DAY, mHour);
317         calendar.set(Calendar.MINUTE, mMinute);
318         calendar.set(Calendar.SECOND, 0);
319         calendar.set(Calendar.MILLISECOND, 0);
320         return calendar;
321     }
322 
323     /**
324      * Return the time when a low priority notification should be shown.
325      *
326      * @return the time
327      */
getLowNotificationTime()328     public Calendar getLowNotificationTime() {
329         Calendar calendar = getAlarmTime();
330         calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET);
331         return calendar;
332     }
333 
334     /**
335      * Return the time when a high priority notification should be shown.
336      *
337      * @return the time
338      */
getHighNotificationTime()339     public Calendar getHighNotificationTime() {
340         Calendar calendar = getAlarmTime();
341         calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET);
342         return calendar;
343     }
344 
345     /**
346      * Return the time when a missed notification should be removed.
347      *
348      * @return the time
349      */
getMissedTimeToLive()350     public Calendar getMissedTimeToLive() {
351         Calendar calendar = getAlarmTime();
352         calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET);
353         return calendar;
354     }
355 
356     /**
357      * Return the time when the alarm should stop firing and be marked as missed.
358      *
359      * @param context to figure out the timeout setting
360      * @return the time when alarm should be silence, or null if never
361      */
getTimeout(Context context)362     public Calendar getTimeout(Context context) {
363         String timeoutSetting = PreferenceManager.getDefaultSharedPreferences(context)
364                 .getString(SettingsActivity.KEY_AUTO_SILENCE, DEFAULT_ALARM_TIMEOUT_SETTING);
365         int timeoutMinutes = Integer.parseInt(timeoutSetting);
366 
367         // Alarm silence has been set to "None"
368         if (timeoutMinutes < 0) {
369             return null;
370         }
371 
372         Calendar calendar = getAlarmTime();
373         calendar.add(Calendar.MINUTE, timeoutMinutes);
374         return calendar;
375     }
376 
377     @Override
equals(Object o)378     public boolean equals(Object o) {
379         if (!(o instanceof AlarmInstance)) return false;
380         final AlarmInstance other = (AlarmInstance) o;
381         return mId == other.mId;
382     }
383 
384     @Override
hashCode()385     public int hashCode() {
386         return Long.valueOf(mId).hashCode();
387     }
388 
389     @Override
toString()390     public String toString() {
391         return "AlarmInstance{" +
392                 "mId=" + mId +
393                 ", mYear=" + mYear +
394                 ", mMonth=" + mMonth +
395                 ", mDay=" + mDay +
396                 ", mHour=" + mHour +
397                 ", mMinute=" + mMinute +
398                 ", mLabel=" + mLabel +
399                 ", mVibrate=" + mVibrate +
400                 ", mRingtone=" + mRingtone +
401                 ", mAlarmId=" + mAlarmId +
402                 ", mAlarmState=" + mAlarmState +
403                 '}';
404     }
405 }
406