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