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