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