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