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