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.Utils;
32import com.android.deskclock.alarms.AlarmStateManager;
33import com.android.deskclock.settings.SettingsActivity;
34
35import java.util.Calendar;
36import java.util.LinkedList;
37import java.util.List;
38
39public 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
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
122    public static Intent createIntent(String action, long instanceId) {
123        return new Intent(action).setData(getUri(instanceId));
124    }
125
126    public static Intent createIntent(Context context, Class<?> cls, long instanceId) {
127        return new Intent(context, cls).setData(getUri(instanceId));
128    }
129
130    public static long getId(Uri contentUri) {
131        return ContentUris.parseId(contentUri);
132    }
133
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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
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     */
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
297    public AlarmInstance(Calendar calendar, Long alarmId) {
298        this(calendar);
299        mAlarmId = alarmId;
300    }
301
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
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
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
359    public String getLabelOrDefault(Context context) {
360        return mLabel.isEmpty() ? context.getString(R.string.default_label) : mLabel;
361    }
362
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     */
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     */
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     */
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     */
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     */
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
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
450    public int hashCode() {
451        return Long.valueOf(mId).hashCode();
452    }
453
454    @Override
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