Alarm.java revision 900ede28311ddf4502622009a3d5ab44a11b7264
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.R; 32import com.android.deskclock.data.DataModel; 33import com.android.deskclock.data.Weekdays; 34 35import java.util.Calendar; 36import java.util.LinkedList; 37import java.util.List; 38 39public 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 Cursor loadInBackground() { 159 // Prime the ringtone title cache for later access. Most alarms will refer to 160 // system ringtones. 161 DataModel.getDataModel().loadRingtoneTitles(); 162 163 return super.loadInBackground(); 164 } 165 }; 166 } 167 168 /** 169 * Get alarm by id. 170 * 171 * @param cr provides access to the content model 172 * @param alarmId for the desired alarm. 173 * @return alarm if found, null otherwise 174 */ 175 public static Alarm getAlarm(ContentResolver cr, long alarmId) { 176 try (Cursor cursor = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null)) { 177 if (cursor.moveToFirst()) { 178 return new Alarm(cursor); 179 } 180 } 181 182 return null; 183 } 184 /** 185 * Get alarm for the {@code contentUri}. 186 * 187 * @param cr provides access to the content model 188 * @param contentUri the {@link #getContentUri deeplink} for the desired alarm 189 * @return instance if found, null otherwise 190 */ 191 public static Alarm getAlarm(ContentResolver cr, Uri contentUri) { 192 return getAlarm(cr, ContentUris.parseId(contentUri)); 193 } 194 195 /** 196 * Get all alarms given conditions. 197 * 198 * @param cr provides access to the content model 199 * @param selection A filter declaring which rows to return, formatted as an 200 * SQL WHERE clause (excluding the WHERE itself). Passing null will 201 * return all rows for the given URI. 202 * @param selectionArgs You may include ?s in selection, which will be 203 * replaced by the values from selectionArgs, in the order that they 204 * appear in the selection. The values will be bound as Strings. 205 * @return list of alarms matching where clause or empty list if none found. 206 */ 207 public static List<Alarm> getAlarms(ContentResolver cr, String selection, 208 String... selectionArgs) { 209 final List<Alarm> result = new LinkedList<>(); 210 try (Cursor cursor = cr.query(CONTENT_URI, QUERY_COLUMNS, selection, selectionArgs, null)) { 211 if (cursor != null && cursor.moveToFirst()) { 212 do { 213 result.add(new Alarm(cursor)); 214 } while (cursor.moveToNext()); 215 } 216 } 217 218 return result; 219 } 220 221 public static boolean isTomorrow(Alarm alarm, Calendar now) { 222 if (alarm.instanceState == AlarmInstance.SNOOZE_STATE) { 223 return false; 224 } 225 226 final int totalAlarmMinutes = alarm.hour * 60 + alarm.minutes; 227 final int totalNowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE); 228 return totalAlarmMinutes <= totalNowMinutes; 229 } 230 231 public static Alarm addAlarm(ContentResolver contentResolver, Alarm alarm) { 232 ContentValues values = createContentValues(alarm); 233 Uri uri = contentResolver.insert(CONTENT_URI, values); 234 alarm.id = getId(uri); 235 return alarm; 236 } 237 238 public static boolean updateAlarm(ContentResolver contentResolver, Alarm alarm) { 239 if (alarm.id == Alarm.INVALID_ID) return false; 240 ContentValues values = createContentValues(alarm); 241 long rowsUpdated = contentResolver.update(getContentUri(alarm.id), values, null, null); 242 return rowsUpdated == 1; 243 } 244 245 public static boolean deleteAlarm(ContentResolver contentResolver, long alarmId) { 246 if (alarmId == INVALID_ID) return false; 247 int deletedRows = contentResolver.delete(getContentUri(alarmId), "", null); 248 return deletedRows == 1; 249 } 250 251 public static final Parcelable.Creator<Alarm> CREATOR = new Parcelable.Creator<Alarm>() { 252 public Alarm createFromParcel(Parcel p) { 253 return new Alarm(p); 254 } 255 256 public Alarm[] newArray(int size) { 257 return new Alarm[size]; 258 } 259 }; 260 261 // Public fields 262 // TODO: Refactor instance names 263 public long id; 264 public boolean enabled; 265 public int hour; 266 public int minutes; 267 public Weekdays daysOfWeek; 268 public boolean vibrate; 269 public String label; 270 public Uri alert; 271 public boolean deleteAfterUse; 272 public int instanceState; 273 public int instanceId; 274 275 // Creates a default alarm at the current time. 276 public Alarm() { 277 this(0, 0); 278 } 279 280 public Alarm(int hour, int minutes) { 281 this.id = INVALID_ID; 282 this.hour = hour; 283 this.minutes = minutes; 284 this.vibrate = true; 285 this.daysOfWeek = Weekdays.NONE; 286 this.label = ""; 287 this.alert = DataModel.getDataModel().getDefaultAlarmRingtoneUri(); 288 this.deleteAfterUse = false; 289 } 290 291 public Alarm(Cursor c) { 292 id = c.getLong(ID_INDEX); 293 enabled = c.getInt(ENABLED_INDEX) == 1; 294 hour = c.getInt(HOUR_INDEX); 295 minutes = c.getInt(MINUTES_INDEX); 296 daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX)); 297 vibrate = c.getInt(VIBRATE_INDEX) == 1; 298 label = c.getString(LABEL_INDEX); 299 deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1; 300 301 if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) { 302 instanceState = c.getInt(INSTANCE_STATE_INDEX); 303 instanceId = c.getInt(INSTANCE_ID_INDEX); 304 } 305 306 if (c.isNull(RINGTONE_INDEX)) { 307 // Should we be saving this with the current ringtone or leave it null 308 // so it changes when user changes default ringtone? 309 alert = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); 310 } else { 311 alert = Uri.parse(c.getString(RINGTONE_INDEX)); 312 } 313 } 314 315 Alarm(Parcel p) { 316 id = p.readLong(); 317 enabled = p.readInt() == 1; 318 hour = p.readInt(); 319 minutes = p.readInt(); 320 daysOfWeek = Weekdays.fromBits(p.readInt()); 321 vibrate = p.readInt() == 1; 322 label = p.readString(); 323 alert = p.readParcelable(null); 324 deleteAfterUse = p.readInt() == 1; 325 } 326 327 /** 328 * @return the deeplink that identifies this alarm 329 */ 330 public Uri getContentUri() { 331 return getContentUri(id); 332 } 333 334 public String getLabelOrDefault(Context context) { 335 return label.isEmpty() ? context.getString(R.string.default_label) : label; 336 } 337 338 /** 339 * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE 340 * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION. 341 */ 342 public boolean canPreemptivelyDismiss() { 343 return instanceState == AlarmInstance.SNOOZE_STATE 344 || instanceState == AlarmInstance.HIGH_NOTIFICATION_STATE 345 || instanceState == AlarmInstance.LOW_NOTIFICATION_STATE 346 || instanceState == AlarmInstance.HIDE_NOTIFICATION_STATE; 347 } 348 349 public void writeToParcel(Parcel p, int flags) { 350 p.writeLong(id); 351 p.writeInt(enabled ? 1 : 0); 352 p.writeInt(hour); 353 p.writeInt(minutes); 354 p.writeInt(daysOfWeek.getBits()); 355 p.writeInt(vibrate ? 1 : 0); 356 p.writeString(label); 357 p.writeParcelable(alert, flags); 358 p.writeInt(deleteAfterUse ? 1 : 0); 359 } 360 361 public int describeContents() { 362 return 0; 363 } 364 365 public AlarmInstance createInstanceAfter(Calendar time) { 366 Calendar nextInstanceTime = getNextAlarmTime(time); 367 AlarmInstance result = new AlarmInstance(nextInstanceTime, id); 368 result.mVibrate = vibrate; 369 result.mLabel = label; 370 result.mRingtone = alert; 371 return result; 372 } 373 374 /** 375 * 376 * @param currentTime the current time 377 * @return previous firing time, or null if this is a one-time alarm. 378 */ 379 public Calendar getPreviousAlarmTime(Calendar currentTime) { 380 final Calendar previousInstanceTime = Calendar.getInstance(currentTime.getTimeZone()); 381 previousInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR)); 382 previousInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH)); 383 previousInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH)); 384 previousInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 385 previousInstanceTime.set(Calendar.MINUTE, minutes); 386 previousInstanceTime.set(Calendar.SECOND, 0); 387 previousInstanceTime.set(Calendar.MILLISECOND, 0); 388 389 final int subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime); 390 if (subtractDays > 0) { 391 previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays); 392 return previousInstanceTime; 393 } else { 394 return null; 395 } 396 } 397 398 public Calendar getNextAlarmTime(Calendar currentTime) { 399 final Calendar nextInstanceTime = Calendar.getInstance(currentTime.getTimeZone()); 400 nextInstanceTime.set(Calendar.YEAR, currentTime.get(Calendar.YEAR)); 401 nextInstanceTime.set(Calendar.MONTH, currentTime.get(Calendar.MONTH)); 402 nextInstanceTime.set(Calendar.DAY_OF_MONTH, currentTime.get(Calendar.DAY_OF_MONTH)); 403 nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 404 nextInstanceTime.set(Calendar.MINUTE, minutes); 405 nextInstanceTime.set(Calendar.SECOND, 0); 406 nextInstanceTime.set(Calendar.MILLISECOND, 0); 407 408 // If we are still behind the passed in currentTime, then add a day 409 if (nextInstanceTime.getTimeInMillis() <= currentTime.getTimeInMillis()) { 410 nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1); 411 } 412 413 // The day of the week might be invalid, so find next valid one 414 final int addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime); 415 if (addDays > 0) { 416 nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays); 417 } 418 419 // Daylight Savings Time can alter the hours and minutes when adjusting the day above. 420 // Reset the desired hour and minute now that the correct day has been chosen. 421 nextInstanceTime.set(Calendar.HOUR_OF_DAY, hour); 422 nextInstanceTime.set(Calendar.MINUTE, minutes); 423 424 return nextInstanceTime; 425 } 426 427 @Override 428 public boolean equals(Object o) { 429 if (!(o instanceof Alarm)) return false; 430 final Alarm other = (Alarm) o; 431 return id == other.id; 432 } 433 434 @Override 435 public int hashCode() { 436 return Long.valueOf(id).hashCode(); 437 } 438 439 @Override 440 public String toString() { 441 return "Alarm{" + 442 "alert=" + alert + 443 ", id=" + id + 444 ", enabled=" + enabled + 445 ", hour=" + hour + 446 ", minutes=" + minutes + 447 ", daysOfWeek=" + daysOfWeek + 448 ", vibrate=" + vibrate + 449 ", label='" + label + '\'' + 450 ", deleteAfterUse=" + deleteAfterUse + 451 '}'; 452 } 453}