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