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