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.CursorLoader;
24import android.content.Intent;
25import android.database.Cursor;
26import android.media.RingtoneManager;
27import android.net.Uri;
28import android.os.Parcel;
29import android.os.Parcelable;
30
31import com.android.deskclock.R;
32
33import java.util.Calendar;
34import java.util.LinkedList;
35import java.util.List;
36
37public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
38    /**
39     * Alarms start with an invalid id when it hasn't been saved to the database.
40     */
41    public static final long INVALID_ID = -1;
42
43    /**
44     * The default sort order for this table
45     */
46    private static final String DEFAULT_SORT_ORDER =
47            HOUR + ", " +
48            MINUTES + " ASC" + ", " +
49            _ID + " DESC";
50
51    private static final String[] QUERY_COLUMNS = {
52            _ID,
53            HOUR,
54            MINUTES,
55            DAYS_OF_WEEK,
56            ENABLED,
57            VIBRATE,
58            LABEL,
59            RINGTONE,
60            DELETE_AFTER_USE
61    };
62
63    /**
64     * These save calls to cursor.getColumnIndexOrThrow()
65     * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
66     */
67    private static final int ID_INDEX = 0;
68    private static final int HOUR_INDEX = 1;
69    private static final int MINUTES_INDEX = 2;
70    private static final int DAYS_OF_WEEK_INDEX = 3;
71    private static final int ENABLED_INDEX = 4;
72    private static final int VIBRATE_INDEX = 5;
73    private static final int LABEL_INDEX = 6;
74    private static final int RINGTONE_INDEX = 7;
75    private static final int DELETE_AFTER_USE_INDEX = 8;
76
77    private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
78
79    public static ContentValues createContentValues(Alarm alarm) {
80        ContentValues values = new ContentValues(COLUMN_COUNT);
81        if (alarm.id != INVALID_ID) {
82            values.put(ClockContract.AlarmsColumns._ID, alarm.id);
83        }
84
85        values.put(ENABLED, alarm.enabled ? 1 : 0);
86        values.put(HOUR, alarm.hour);
87        values.put(MINUTES, alarm.minutes);
88        values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBitSet());
89        values.put(VIBRATE, alarm.vibrate ? 1 : 0);
90        values.put(LABEL, alarm.label);
91        values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
92        if (alarm.alert == null) {
93            // We want to put null, so default alarm changes
94            values.putNull(RINGTONE);
95        } else {
96            values.put(RINGTONE, alarm.alert.toString());
97        }
98
99        return values;
100    }
101
102    public static Intent createIntent(String action, long alarmId) {
103        return new Intent(action).setData(getUri(alarmId));
104    }
105
106    public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
107        return new Intent(context, cls).setData(getUri(alarmId));
108    }
109
110    public static Uri getUri(long alarmId) {
111        return ContentUris.withAppendedId(CONTENT_URI, alarmId);
112    }
113
114    public static long getId(Uri contentUri) {
115        return ContentUris.parseId(contentUri);
116    }
117
118    /**
119     * Get alarm cursor loader for all alarms.
120     *
121     * @param context to query the database.
122     * @return cursor loader with all the alarms.
123     */
124    public static CursorLoader getAlarmsCursorLoader(Context context) {
125        return new CursorLoader(context, ClockContract.AlarmsColumns.CONTENT_URI,
126                QUERY_COLUMNS, null, null, DEFAULT_SORT_ORDER);
127    }
128
129    /**
130     * Get alarm by id.
131     *
132     * @param contentResolver to perform the query on.
133     * @param alarmId for the desired alarm.
134     * @return alarm if found, null otherwise
135     */
136    public static Alarm getAlarm(ContentResolver contentResolver, long alarmId) {
137        Cursor cursor = contentResolver.query(getUri(alarmId), QUERY_COLUMNS, null, null, null);
138        Alarm result = null;
139        if (cursor == null) {
140            return result;
141        }
142
143        try {
144            if (cursor.moveToFirst()) {
145                result = new Alarm(cursor);
146            }
147        } finally {
148            cursor.close();
149        }
150
151        return result;
152    }
153
154    /**
155     * Get all alarms given conditions.
156     *
157     * @param contentResolver to perform the query on.
158     * @param selection A filter declaring which rows to return, formatted as an
159     *         SQL WHERE clause (excluding the WHERE itself). Passing null will
160     *         return all rows for the given URI.
161     * @param selectionArgs You may include ?s in selection, which will be
162     *         replaced by the values from selectionArgs, in the order that they
163     *         appear in the selection. The values will be bound as Strings.
164     * @return list of alarms matching where clause or empty list if none found.
165     */
166    public static List<Alarm> getAlarms(ContentResolver contentResolver,
167            String selection, String ... selectionArgs) {
168        Cursor cursor  = contentResolver.query(CONTENT_URI, QUERY_COLUMNS,
169                selection, selectionArgs, null);
170        List<Alarm> result = new LinkedList<Alarm>();
171        if (cursor == null) {
172            return result;
173        }
174
175        try {
176            if (cursor.moveToFirst()) {
177                do {
178                    result.add(new Alarm(cursor));
179                } while (cursor.moveToNext());
180            }
181        } finally {
182            cursor.close();
183        }
184
185        return result;
186    }
187
188    public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
189        ContentValues values = createContentValues(alarm);
190        Uri uri = contentResolver.insert(CONTENT_URI, values);
191        alarm.id = getId(uri);
192        return alarm;
193    }
194
195    public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
196        if (alarm.id == Alarm.INVALID_ID) return false;
197        ContentValues values = createContentValues(alarm);
198        long rowsUpdated = contentResolver.update(getUri(alarm.id), values, null, null);
199        return rowsUpdated == 1;
200    }
201
202    public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
203        if (alarmId == INVALID_ID) return false;
204        int deletedRows = contentResolver.delete(getUri(alarmId), "", null);
205        return deletedRows == 1;
206    }
207
208    public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
209        public Alarm createFromParcel(Parcel p) {
210            return new Alarm(p);
211        }
212
213        public Alarm[] newArray(int size) {
214            return new Alarm[size];
215        }
216    };
217
218    // Public fields
219    // TODO: Refactor instance names
220    public long id;
221    public boolean enabled;
222    public int hour;
223    public int minutes;
224    public DaysOfWeek daysOfWeek;
225    public boolean vibrate;
226    public String label;
227    public Uri alert;
228    public boolean deleteAfterUse;
229
230    // Creates a default alarm at the current time.
231    public Alarm() {
232        this(0, 0);
233    }
234
235    public Alarm(int hour, int minutes) {
236        this.id = INVALID_ID;
237        this.hour = hour;
238        this.minutes = minutes;
239        this.vibrate = true;
240        this.daysOfWeek = new DaysOfWeek(0);
241        this.label = "";
242        this.alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
243        this.deleteAfterUse = false;
244    }
245
246    public Alarm(Cursor c) {
247        id = c.getLong(ID_INDEX);
248        enabled = c.getInt(ENABLED_INDEX) == 1;
249        hour = c.getInt(HOUR_INDEX);
250        minutes = c.getInt(MINUTES_INDEX);
251        daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX));
252        vibrate = c.getInt(VIBRATE_INDEX) == 1;
253        label = c.getString(LABEL_INDEX);
254        deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
255
256        if (c.isNull(RINGTONE_INDEX)) {
257            // Should we be saving this with the current ringtone or leave it null
258            // so it changes when user changes default ringtone?
259            alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
260        } else {
261            alert = Uri.parse(c.getString(RINGTONE_INDEX));
262        }
263    }
264
265    Alarm(Parcel p) {
266        id = p.readLong();
267        enabled = p.readInt() == 1;
268        hour = p.readInt();
269        minutes = p.readInt();
270        daysOfWeek = new DaysOfWeek(p.readInt());
271        vibrate = p.readInt() == 1;
272        label = p.readString();
273        alert = (Uri) p.readParcelable(null);
274        deleteAfterUse = p.readInt() == 1;
275    }
276
277    public String getLabelOrDefault(Context context) {
278        if (label == null || label.length() == 0) {
279            return context.getString(R.string.default_label);
280        }
281        return label;
282    }
283
284    public void writeToParcel(Parcel p, int flags) {
285        p.writeLong(id);
286        p.writeInt(enabled ? 1 : 0);
287        p.writeInt(hour);
288        p.writeInt(minutes);
289        p.writeInt(daysOfWeek.getBitSet());
290        p.writeInt(vibrate ? 1 : 0);
291        p.writeString(label);
292        p.writeParcelable(alert, flags);
293        p.writeInt(deleteAfterUse ? 1 : 0);
294    }
295
296    public int describeContents() {
297        return 0;
298    }
299
300    public AlarmInstance createInstanceAfter(Calendar time) {
301        Calendar nextInstanceTime = Calendar.getInstance();
302        nextInstanceTime.set(Calendar.YEAR, time.get(Calendar.YEAR));
303        nextInstanceTime.set(Calendar.MONTH, time.get(Calendar.MONTH));
304        nextInstanceTime.set(Calendar.DAY_OF_MONTH, time.get(Calendar.DAY_OF_MONTH));
305        nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
306        nextInstanceTime.set(Calendar.MINUTE, minutes);
307        nextInstanceTime.set(Calendar.SECOND, 0);
308        nextInstanceTime.set(Calendar.MILLISECOND, 0);
309
310        // If we are still behind the passed in time, then add a day
311        if (nextInstanceTime.getTimeInMillis() <= time.getTimeInMillis()) {
312            nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
313        }
314
315        // The day of the week might be invalid, so find next valid one
316        int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime);
317        if (addDays > 0) {
318            nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
319        }
320
321        AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
322        result.mVibrate = vibrate;
323        result.mLabel = label;
324        result.mRingtone = alert;
325        return result;
326    }
327
328    @Override
329    public boolean equals(Object o) {
330        if (!(o instanceof Alarm)) return false;
331        final Alarm other = (Alarm) o;
332        return id == other.id;
333    }
334
335    @Override
336    public int hashCode() {
337        return Long.valueOf(id).hashCode();
338    }
339
340    @Override
341    public String toString() {
342        return "Alarm{" +
343                "alert=" + alert +
344                ", id=" + id +
345                ", enabled=" + enabled +
346                ", hour=" + hour +
347                ", minutes=" + minutes +
348                ", daysOfWeek=" + daysOfWeek +
349                ", vibrate=" + vibrate +
350                ", label='" + label + '\'' +
351                ", deleteAfterUse=" + deleteAfterUse +
352                '}';
353    }
354}
355