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.Intent;
     24 import android.database.Cursor;
     25 import android.media.RingtoneManager;
     26 import android.net.Uri;
     27 
     28 import com.android.deskclock.LogUtils;
     29 import com.android.deskclock.R;
     30 import com.android.deskclock.alarms.AlarmStateManager;
     31 import com.android.deskclock.data.DataModel;
     32 
     33 import java.util.Calendar;
     34 import java.util.LinkedList;
     35 import java.util.List;
     36 
     37 public 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