Home | History | Annotate | Download | only in provider
      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 
     17 package com.android.deskclock.provider;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.ContentUris;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.content.CursorLoader;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.media.RingtoneManager;
     27 import android.net.Uri;
     28 import android.os.Parcel;
     29 import android.os.Parcelable;
     30 
     31 import com.android.deskclock.R;
     32 import com.android.deskclock.data.DataModel;
     33 
     34 import java.util.Calendar;
     35 import java.util.LinkedList;
     36 import java.util.List;
     37 
     38 public final class Alarm implements Parcelable, ClockContract.AlarmsColumns {
     39     /**
     40      * Alarms start with an invalid id when it hasn't been saved to the database.
     41      */
     42     public static final long INVALID_ID = -1;
     43 
     44     /**
     45      * The default sort order for this table
     46      */
     47     private static final String DEFAULT_SORT_ORDER =
     48             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR + ", " +
     49             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." +  MINUTES + " ASC" + ", " +
     50             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ClockContract.AlarmsColumns._ID + " DESC";
     51 
     52     private static final String[] QUERY_COLUMNS = {
     53             _ID,
     54             HOUR,
     55             MINUTES,
     56             DAYS_OF_WEEK,
     57             ENABLED,
     58             VIBRATE,
     59             LABEL,
     60             RINGTONE,
     61             DELETE_AFTER_USE
     62     };
     63 
     64     private static final String[] QUERY_ALARMS_WITH_INSTANCES_COLUMNS = {
     65             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + _ID,
     66             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + HOUR,
     67             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + MINUTES,
     68             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DAYS_OF_WEEK,
     69             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + ENABLED,
     70             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + VIBRATE,
     71             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + LABEL,
     72             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + RINGTONE,
     73             ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + DELETE_AFTER_USE,
     74             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "."
     75                     + ClockContract.InstancesColumns.ALARM_STATE,
     76             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns._ID,
     77             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.YEAR,
     78             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MONTH,
     79             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.DAY,
     80             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.HOUR,
     81             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.MINUTES,
     82             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.LABEL,
     83             ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + ClockContract.InstancesColumns.VIBRATE
     84     };
     85 
     86     /**
     87      * These save calls to cursor.getColumnIndexOrThrow()
     88      * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
     89      */
     90     private static final int ID_INDEX = 0;
     91     private static final int HOUR_INDEX = 1;
     92     private static final int MINUTES_INDEX = 2;
     93     private static final int DAYS_OF_WEEK_INDEX = 3;
     94     private static final int ENABLED_INDEX = 4;
     95     private static final int VIBRATE_INDEX = 5;
     96     private static final int LABEL_INDEX = 6;
     97     private static final int RINGTONE_INDEX = 7;
     98     private static final int DELETE_AFTER_USE_INDEX = 8;
     99     private static final int INSTANCE_STATE_INDEX = 9;
    100     public static final int INSTANCE_ID_INDEX = 10;
    101     public static final int INSTANCE_YEAR_INDEX = 11;
    102     public static final int INSTANCE_MONTH_INDEX = 12;
    103     public static final int INSTANCE_DAY_INDEX = 13;
    104     public static final int INSTANCE_HOUR_INDEX = 14;
    105     public static final int INSTANCE_MINUTE_INDEX = 15;
    106     public static final int INSTANCE_LABEL_INDEX = 16;
    107     public static final int INSTANCE_VIBRATE_INDEX = 17;
    108 
    109     private static final int COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1;
    110     private static final int ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1;
    111 
    112     public static ContentValues createContentValues(Alarm alarm) {
    113         ContentValues values = new ContentValues(COLUMN_COUNT);
    114         if (alarm.id != INVALID_ID) {
    115             values.put(ClockContract.AlarmsColumns._ID, alarm.id);
    116         }
    117 
    118         values.put(ENABLED, alarm.enabled ? 1 : 0);
    119         values.put(HOUR, alarm.hour);
    120         values.put(MINUTES, alarm.minutes);
    121         values.put(DAYS_OF_WEEK, alarm.daysOfWeek.getBitSet());
    122         values.put(VIBRATE, alarm.vibrate ? 1 : 0);
    123         values.put(LABEL, alarm.label);
    124         values.put(DELETE_AFTER_USE, alarm.deleteAfterUse);
    125         if (alarm.alert == null) {
    126             // We want to put null, so default alarm changes
    127             values.putNull(RINGTONE);
    128         } else {
    129             values.put(RINGTONE, alarm.alert.toString());
    130         }
    131 
    132         return values;
    133     }
    134 
    135     public static Intent createIntent(Context context, Class<?> cls, long alarmId) {
    136         return new Intent(context, cls).setData(getUri(alarmId));
    137     }
    138 
    139     public static Uri getUri(long alarmId) {
    140         return ContentUris.withAppendedId(CONTENT_URI, alarmId);
    141     }
    142 
    143     public static long getId(Uri contentUri) {
    144         return ContentUris.parseId(contentUri);
    145     }
    146 
    147     /**
    148      * Get alarm cursor loader for all alarms.
    149      *
    150      * @param context to query the database.
    151      * @return cursor loader with all the alarms.
    152      */
    153     public static CursorLoader getAlarmsCursorLoader(Context context) {
    154         return new CursorLoader(context, ALARMS_WITH_INSTANCES_URI,
    155                 QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER);
    156     }
    157 
    158     /**
    159      * Get alarm by id.
    160      *
    161      * @param cr to perform the query on.
    162      * @param alarmId for the desired alarm.
    163      * @return alarm if found, null otherwise
    164      */
    165     public static Alarm getAlarm(ContentResolver cr, long alarmId) {
    166         try (Cursor cursor = cr.query(getUri(alarmId), QUERY_COLUMNS, null, null, null)) {
    167             if (cursor.moveToFirst()) {
    168                 return new Alarm(cursor);
    169             }
    170         }
    171 
    172         return null;
    173     }
    174 
    175     /**
    176      * Get all alarms given conditions.
    177      *
    178      * @param cr to perform the query on.
    179      * @param selection A filter declaring which rows to return, formatted as an
    180      *         SQL WHERE clause (excluding the WHERE itself). Passing null will
    181      *         return all rows for the given URI.
    182      * @param selectionArgs You may include ?s in selection, which will be
    183      *         replaced by the values from selectionArgs, in the order that they
    184      *         appear in the selection. The values will be bound as Strings.
    185      * @return list of alarms matching where clause or empty list if none found.
    186      */
    187     public static List<Alarm> getAlarms(ContentResolver cr, String selection,
    188             String... selectionArgs) {
    189         final List<Alarm> result = new LinkedList<>();
    190         try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) {
    191             if (cursor != null && cursor.moveToFirst()) {
    192                 do {
    193                     result.add(new Alarm(cursor));
    194                 } while (cursor.moveToNext());
    195             }
    196         }
    197 
    198         return result;
    199     }
    200 
    201     public static boolean isTomorrow(Alarm alarm, Calendar now) {
    202         final int alarmHour = alarm.hour;
    203         final int currHour = now.get(Calendar.HOUR_OF_DAY);
    204         // If the alarm is not snoozed and the time is less than the current time, it must be
    205         // firing tomorrow.
    206         // If the alarm is snoozed, return "false" to indicate that this alarm is firing today.
    207         // (The alarm must have already rung today in order to be snoozed, and this function is only
    208         // called on non-repeating alarms.)
    209         return alarm.instanceState != AlarmInstance.SNOOZE_STATE
    210                 && (alarmHour < currHour
    211                 || (alarmHour == currHour && alarm.minutes <= now.get(Calendar.MINUTE)));
    212     }
    213 
    214     public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) {
    215         ContentValues values = createContentValues(alarm);
    216         Uri uri = contentResolver.insert(CONTENT_URI, values);
    217         alarm.id = getId(uri);
    218         return alarm;
    219     }
    220 
    221     public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) {
    222         if (alarm.id == Alarm.INVALID_ID) return false;
    223         ContentValues values = createContentValues(alarm);
    224         long rowsUpdated = contentResolver.update(getUri(alarm.id), values, null, null);
    225         return rowsUpdated == 1;
    226     }
    227 
    228     public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) {
    229         if (alarmId == INVALID_ID) return false;
    230         int deletedRows = contentResolver.delete(getUri(alarmId), "", null);
    231         return deletedRows == 1;
    232     }
    233 
    234     public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() {
    235         public Alarm createFromParcel(Parcel p) {
    236             return new Alarm(p);
    237         }
    238 
    239         public Alarm[] newArray(int size) {
    240             return new Alarm[size];
    241         }
    242     };
    243 
    244     // Public fields
    245     // TODO: Refactor instance names
    246     public long id;
    247     public boolean enabled;
    248     public int hour;
    249     public int minutes;
    250     public DaysOfWeek daysOfWeek;
    251     public boolean vibrate;
    252     public String label;
    253     public Uri alert;
    254     public boolean deleteAfterUse;
    255     public int instanceState;
    256     public int instanceId;
    257 
    258     // Creates a default alarm at the current time.
    259     public Alarm() {
    260         this(0, 0);
    261     }
    262 
    263     public Alarm(int hour, int minutes) {
    264         this.id = INVALID_ID;
    265         this.hour = hour;
    266         this.minutes = minutes;
    267         this.vibrate = true;
    268         this.daysOfWeek = new DaysOfWeek(0);
    269         this.label = "";
    270         this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri();
    271         this.deleteAfterUse = false;
    272     }
    273 
    274     public Alarm(Cursor c) {
    275         id = c.getLong(ID_INDEX);
    276         enabled = c.getInt(ENABLED_INDEX) == 1;
    277         hour = c.getInt(HOUR_INDEX);
    278         minutes = c.getInt(MINUTES_INDEX);
    279         daysOfWeek = new DaysOfWeek(c.getInt(DAYS_OF_WEEK_INDEX));
    280         vibrate = c.getInt(VIBRATE_INDEX) == 1;
    281         label = c.getString(LABEL_INDEX);
    282         deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1;
    283 
    284         if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) {
    285             instanceState = c.getInt(INSTANCE_STATE_INDEX);
    286             instanceId = c.getInt(INSTANCE_ID_INDEX);
    287         }
    288 
    289         if (c.isNull(RINGTONE_INDEX)) {
    290             // Should we be saving this with the current ringtone or leave it null
    291             // so it changes when user changes default ringtone?
    292             alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM);
    293         } else {
    294             alert = Uri.parse(c.getString(RINGTONE_INDEX));
    295         }
    296     }
    297 
    298     Alarm(Parcel p) {
    299         id = p.readLong();
    300         enabled = p.readInt() == 1;
    301         hour = p.readInt();
    302         minutes = p.readInt();
    303         daysOfWeek = new DaysOfWeek(p.readInt());
    304         vibrate = p.readInt() == 1;
    305         label = p.readString();
    306         alert = p.readParcelable(null);
    307         deleteAfterUse = p.readInt() == 1;
    308     }
    309 
    310     public String getLabelOrDefault(Context context) {
    311         return label.isEmpty() ? context.getString(R.string.default_label) : label;
    312     }
    313 
    314     /**
    315      * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE
    316      * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION.
    317      */
    318     public boolean canPreemptivelyDismiss() {
    319         return instanceState == AlarmInstance.SNOOZE_STATE
    320                 || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE
    321                 || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE
    322                 || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE;
    323     }
    324 
    325     public void writeToParcel(Parcel p, int flags) {
    326         p.writeLong(id);
    327         p.writeInt(enabled ? 1 : 0);
    328         p.writeInt(hour);
    329         p.writeInt(minutes);
    330         p.writeInt(daysOfWeek.getBitSet());
    331         p.writeInt(vibrate ? 1 : 0);
    332         p.writeString(label);
    333         p.writeParcelable(alert, flags);
    334         p.writeInt(deleteAfterUse ? 1 : 0);
    335     }
    336 
    337     public int describeContents() {
    338         return 0;
    339     }
    340 
    341     public AlarmInstance createInstanceAfter(Calendar time) {
    342         Calendar nextInstanceTime = getNextAlarmTime(time);
    343         AlarmInstance result = new AlarmInstance(nextInstanceTime, id);
    344         result.mVibrate = vibrate;
    345         result.mLabel = label;
    346         result.mRingtone = alert;
    347         return result;
    348     }
    349 
    350     /**
    351      *
    352      * @param currentTime
    353      * @return Previous firing time, or null if this is a one-time alarm.
    354      */
    355     public Calendar getPreviousAlarmTime(Calendar currentTime) {
    356         Calendar previousInstanceTime = Calendar.getInstance();
    357         previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
    358         previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
    359         previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
    360         previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
    361         previousInstanceTime.set(Calendar.MINUTE, minutes);
    362         previousInstanceTime.set(Calendar.SECOND, 0);
    363         previousInstanceTime.set(Calendar.MILLISECOND, 0);
    364 
    365         int subtractDays = daysOfWeek.calculateDaysToPreviousAlarm(previousInstanceTime);
    366         if (subtractDays > 0) {
    367             previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays);
    368             return previousInstanceTime;
    369         } else {
    370             return null;
    371         }
    372     }
    373 
    374     public Calendar getNextAlarmTime(Calendar currentTime) {
    375         final Calendar nextInstanceTime = Calendar.getInstance();
    376         nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR));
    377         nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH));
    378         nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH));
    379         nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
    380         nextInstanceTime.set(Calendar.MINUTE, minutes);
    381         nextInstanceTime.set(Calendar.SECOND, 0);
    382         nextInstanceTime.set(Calendar.MILLISECOND, 0);
    383 
    384         // If we are still behind the passed in currentTime, then add a day
    385         if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) {
    386             nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1);
    387         }
    388 
    389         // The day of the week might be invalid, so find next valid one
    390         int addDays = daysOfWeek.calculateDaysToNextAlarm(nextInstanceTime);
    391         if (addDays > 0) {
    392             nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays);
    393         }
    394 
    395         // Daylight Savings Time can alter the hours and minutes when adjusting the day above.
    396         // Reset the desired hour and minute now that the correct day has been chosen.
    397         nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour);
    398         nextInstanceTime.set(Calendar.MINUTE, minutes);
    399 
    400         return nextInstanceTime;
    401     }
    402 
    403     @Override
    404     public boolean equals(Object o) {
    405         if (!(o instanceof Alarm)) return false;
    406         final Alarm other = (Alarm) o;
    407         return id == other.id;
    408     }
    409 
    410     @Override
    411     public int hashCode() {
    412         return Long.valueOf(id).hashCode();
    413     }
    414 
    415     @Override
    416     public String toString() {
    417         return "Alarm{" +
    418                 "alert=" + alert +
    419                 ", id=" + id +
    420                 ", enabled=" + enabled +
    421                 ", hour=" + hour +
    422                 ", minutes=" + minutes +
    423                 ", daysOfWeek=" + daysOfWeek +
    424                 ", vibrate=" + vibrate +
    425                 ", label='" + label + '\'' +
    426                 ", deleteAfterUse=" + deleteAfterUse +
    427                 '}';
    428     }
    429 }
    430