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