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