1 /* 2 * Copyright (C) 2020 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 import android.provider.BaseColumns 31 32 import com.android.deskclock.R 33 import com.android.deskclock.data.DataModel 34 import com.android.deskclock.data.Weekdays 35 import com.android.deskclock.provider.ClockContract.AlarmSettingColumns 36 import com.android.deskclock.provider.ClockContract.AlarmsColumns 37 import com.android.deskclock.provider.ClockContract.InstancesColumns 38 39 import java.util.Calendar 40 import java.util.LinkedList 41 42 // TODO(colinmarsch) Replace deprecated CursorLoader usages here 43 class Alarm : Parcelable, AlarmsColumns { 44 // Public fields 45 // TODO: Refactor instance names 46 @JvmField 47 var id: Long 48 49 @JvmField 50 var enabled = false 51 52 @JvmField 53 var hour: Int 54 55 @JvmField 56 var minutes: Int 57 58 @JvmField 59 var daysOfWeek: Weekdays 60 61 @JvmField 62 var vibrate: Boolean 63 64 @JvmField 65 var label: String? 66 67 @JvmField 68 var alert: Uri? = null 69 70 @JvmField 71 var deleteAfterUse: Boolean 72 73 @JvmField 74 var instanceState = 0 75 76 var instanceId = 0 77 78 // Creates a default alarm at the current time. 79 @JvmOverloads 80 constructor(hour: Int = 0, minutes: Int = 0) { 81 id = INVALID_ID 82 this.hour = hour 83 this.minutes = minutes 84 vibrate = true 85 daysOfWeek = Weekdays.NONE 86 label = "" 87 alert = DataModel.dataModel.defaultAlarmRingtoneUri 88 deleteAfterUse = false 89 } 90 91 constructor(c: Cursor) { 92 id = c.getLong(ID_INDEX) 93 enabled = c.getInt(ENABLED_INDEX) == 1 94 hour = c.getInt(HOUR_INDEX) 95 minutes = c.getInt(MINUTES_INDEX) 96 daysOfWeek = Weekdays.fromBits(c.getInt(DAYS_OF_WEEK_INDEX)) 97 vibrate = c.getInt(VIBRATE_INDEX) == 1 98 label = c.getString(LABEL_INDEX) 99 deleteAfterUse = c.getInt(DELETE_AFTER_USE_INDEX) == 1 100 101 if (c.getColumnCount() == ALARM_JOIN_INSTANCE_COLUMN_COUNT) { 102 instanceState = c.getInt(INSTANCE_STATE_INDEX) 103 instanceId = c.getInt(INSTANCE_ID_INDEX) 104 } 105 106 alert = if (c.isNull(RINGTONE_INDEX)) { 107 // Should we be saving this with the current ringtone or leave it null 108 // so it changes when user changes default ringtone? 109 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) 110 } else { 111 Uri.parse(c.getString(RINGTONE_INDEX)) 112 } 113 } 114 115 internal constructor(p: Parcel) { 116 id = p.readLong() 117 enabled = p.readInt() == 1 118 hour = p.readInt() 119 minutes = p.readInt() 120 daysOfWeek = Weekdays.fromBits(p.readInt()) 121 vibrate = p.readInt() == 1 122 label = p.readString() 123 alert = p.readParcelable(null) 124 deleteAfterUse = p.readInt() == 1 125 } 126 127 /** 128 * @return the deeplink that identifies this alarm 129 */ 130 val contentUri: Uri 131 get() = getContentUri(id) 132 getLabelOrDefaultnull133 fun getLabelOrDefault(context: Context): String { 134 return if (label.isNullOrEmpty()) context.getString(R.string.default_label) else label!! 135 } 136 137 /** 138 * Whether the alarm is in a state to show preemptive dismiss. Valid states are SNOOZE_STATE 139 * HIGH_NOTIFICATION, LOW_NOTIFICATION, and HIDE_NOTIFICATION. 140 */ canPreemptivelyDismissnull141 fun canPreemptivelyDismiss(): Boolean { 142 return instanceState == InstancesColumns.SNOOZE_STATE || 143 instanceState == InstancesColumns.HIGH_NOTIFICATION_STATE || 144 instanceState == InstancesColumns.LOW_NOTIFICATION_STATE || 145 instanceState == InstancesColumns.HIDE_NOTIFICATION_STATE 146 } 147 writeToParcelnull148 override fun writeToParcel(p: Parcel, flags: Int) { 149 p.writeLong(id) 150 p.writeInt(if (enabled) 1 else 0) 151 p.writeInt(hour) 152 p.writeInt(minutes) 153 p.writeInt(daysOfWeek.bits) 154 p.writeInt(if (vibrate) 1 else 0) 155 p.writeString(label) 156 p.writeParcelable(alert, flags) 157 p.writeInt(if (deleteAfterUse) 1 else 0) 158 } 159 describeContentsnull160 override fun describeContents(): Int = 0 161 162 fun createInstanceAfter(time: Calendar): AlarmInstance { 163 val nextInstanceTime = getNextAlarmTime(time) 164 val result = AlarmInstance(nextInstanceTime, id) 165 result.mVibrate = vibrate 166 result.mLabel = label 167 result.mRingtone = alert 168 return result 169 } 170 171 /** 172 * 173 * @param currentTime the current time 174 * @return previous firing time, or null if this is a one-time alarm. 175 */ getPreviousAlarmTimenull176 fun getPreviousAlarmTime(currentTime: Calendar): Calendar? { 177 val previousInstanceTime = Calendar.getInstance(currentTime.timeZone) 178 previousInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR] 179 previousInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH] 180 previousInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH] 181 previousInstanceTime[Calendar.HOUR_OF_DAY] = hour 182 previousInstanceTime[Calendar.MINUTE] = minutes 183 previousInstanceTime[Calendar.SECOND] = 0 184 previousInstanceTime[Calendar.MILLISECOND] = 0 185 186 val subtractDays = daysOfWeek.getDistanceToPreviousDay(previousInstanceTime) 187 return if (subtractDays > 0) { 188 previousInstanceTime.add(Calendar.DAY_OF_WEEK, -subtractDays) 189 previousInstanceTime 190 } else { 191 null 192 } 193 } 194 getNextAlarmTimenull195 fun getNextAlarmTime(currentTime: Calendar): Calendar { 196 val nextInstanceTime = Calendar.getInstance(currentTime.timeZone) 197 nextInstanceTime[Calendar.YEAR] = currentTime[Calendar.YEAR] 198 nextInstanceTime[Calendar.MONTH] = currentTime[Calendar.MONTH] 199 nextInstanceTime[Calendar.DAY_OF_MONTH] = currentTime[Calendar.DAY_OF_MONTH] 200 nextInstanceTime[Calendar.HOUR_OF_DAY] = hour 201 nextInstanceTime[Calendar.MINUTE] = minutes 202 nextInstanceTime[Calendar.SECOND] = 0 203 nextInstanceTime[Calendar.MILLISECOND] = 0 204 205 // If we are still behind the passed in currentTime, then add a day 206 if (nextInstanceTime.timeInMillis <= currentTime.timeInMillis) { 207 nextInstanceTime.add(Calendar.DAY_OF_YEAR, 1) 208 } 209 210 // The day of the week might be invalid, so find next valid one 211 val addDays = daysOfWeek.getDistanceToNextDay(nextInstanceTime) 212 if (addDays > 0) { 213 nextInstanceTime.add(Calendar.DAY_OF_WEEK, addDays) 214 } 215 216 // Daylight Savings Time can alter the hours and minutes when adjusting the day above. 217 // Reset the desired hour and minute now that the correct day has been chosen. 218 nextInstanceTime[Calendar.HOUR_OF_DAY] = hour 219 nextInstanceTime[Calendar.MINUTE] = minutes 220 221 return nextInstanceTime 222 } 223 equalsnull224 override fun equals(other: Any?): Boolean { 225 if (other !is Alarm) return false 226 return id == other.id 227 } 228 hashCodenull229 override fun hashCode(): Int { 230 return java.lang.Long.valueOf(id).hashCode() 231 } 232 toStringnull233 override fun toString(): String { 234 return "Alarm{" + 235 "alert=" + alert + 236 ", id=" + id + 237 ", enabled=" + enabled + 238 ", hour=" + hour + 239 ", minutes=" + minutes + 240 ", daysOfWeek=" + daysOfWeek + 241 ", vibrate=" + vibrate + 242 ", label='" + label + '\'' + 243 ", deleteAfterUse=" + deleteAfterUse + 244 '}' 245 } 246 247 companion object { 248 /** 249 * Alarms start with an invalid id when it hasn't been saved to the database. 250 */ 251 const val INVALID_ID: Long = -1 252 253 /** 254 * The default sort order for this table 255 */ 256 private val DEFAULT_SORT_ORDER = ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + 257 AlarmsColumns.HOUR + ", " + ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + 258 AlarmsColumns.MINUTES + " ASC" + ", " + 259 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID + " DESC" 260 261 private val QUERY_COLUMNS = arrayOf( 262 BaseColumns._ID, 263 AlarmsColumns.HOUR, 264 AlarmsColumns.MINUTES, 265 AlarmsColumns.DAYS_OF_WEEK, 266 AlarmsColumns.ENABLED, 267 AlarmSettingColumns.VIBRATE, 268 AlarmSettingColumns.LABEL, 269 AlarmSettingColumns.RINGTONE, 270 AlarmsColumns.DELETE_AFTER_USE 271 ) 272 273 private val QUERY_ALARMS_WITH_INSTANCES_COLUMNS = arrayOf( 274 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + BaseColumns._ID, 275 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR, 276 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES, 277 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK, 278 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED, 279 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE, 280 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL, 281 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE, 282 ClockDatabaseHelper.ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE, 283 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE, 284 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + BaseColumns._ID, 285 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR, 286 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH, 287 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY, 288 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR, 289 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES, 290 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL, 291 ClockDatabaseHelper.INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE 292 ) 293 294 /** 295 * These save calls to cursor.getColumnIndexOrThrow() 296 * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS 297 */ 298 private const val ID_INDEX = 0 299 private const val HOUR_INDEX = 1 300 private const val MINUTES_INDEX = 2 301 private const val DAYS_OF_WEEK_INDEX = 3 302 private const val ENABLED_INDEX = 4 303 private const val VIBRATE_INDEX = 5 304 private const val LABEL_INDEX = 6 305 private const val RINGTONE_INDEX = 7 306 private const val DELETE_AFTER_USE_INDEX = 8 307 private const val INSTANCE_STATE_INDEX = 9 308 const val INSTANCE_ID_INDEX = 10 309 const val INSTANCE_YEAR_INDEX = 11 310 const val INSTANCE_MONTH_INDEX = 12 311 const val INSTANCE_DAY_INDEX = 13 312 const val INSTANCE_HOUR_INDEX = 14 313 const val INSTANCE_MINUTE_INDEX = 15 314 const val INSTANCE_LABEL_INDEX = 16 315 const val INSTANCE_VIBRATE_INDEX = 17 316 317 private const val COLUMN_COUNT = DELETE_AFTER_USE_INDEX + 1 318 private const val ALARM_JOIN_INSTANCE_COLUMN_COUNT = INSTANCE_VIBRATE_INDEX + 1 319 320 @JvmStatic createContentValuesnull321 fun createContentValues(alarm: Alarm): ContentValues { 322 val values = ContentValues(COLUMN_COUNT) 323 if (alarm.id != INVALID_ID) { 324 values.put(BaseColumns._ID, alarm.id) 325 } 326 327 values.put(AlarmsColumns.ENABLED, if (alarm.enabled) 1 else 0) 328 values.put(AlarmsColumns.HOUR, alarm.hour) 329 values.put(AlarmsColumns.MINUTES, alarm.minutes) 330 values.put(AlarmsColumns.DAYS_OF_WEEK, alarm.daysOfWeek.bits) 331 values.put(AlarmSettingColumns.VIBRATE, if (alarm.vibrate) 1 else 0) 332 values.put(AlarmSettingColumns.LABEL, alarm.label) 333 values.put(AlarmsColumns.DELETE_AFTER_USE, alarm.deleteAfterUse) 334 if (alarm.alert == null) { 335 // We want to put null, so default alarm changes 336 values.putNull(AlarmSettingColumns.RINGTONE) 337 } else { 338 values.put(AlarmSettingColumns.RINGTONE, alarm.alert.toString()) 339 } 340 return values 341 } 342 343 @JvmStatic createIntentnull344 fun createIntent(context: Context?, cls: Class<*>?, alarmId: Long): Intent { 345 return Intent(context, cls).setData(getContentUri(alarmId)) 346 } 347 getContentUrinull348 fun getContentUri(alarmId: Long): Uri { 349 return ContentUris.withAppendedId(AlarmsColumns.CONTENT_URI, alarmId) 350 } 351 getIdnull352 fun getId(contentUri: Uri): Long { 353 return ContentUris.parseId(contentUri) 354 } 355 356 /** 357 * Get alarm cursor loader for all alarms. 358 * 359 * @param context to query the database. 360 * @return cursor loader with all the alarms. 361 */ 362 @JvmStatic getAlarmsCursorLoadernull363 fun getAlarmsCursorLoader(context: Context?): CursorLoader { 364 return object : CursorLoader(context, AlarmsColumns.ALARMS_WITH_INSTANCES_URI, 365 QUERY_ALARMS_WITH_INSTANCES_COLUMNS, null, null, DEFAULT_SORT_ORDER) { 366 367 override fun onContentChanged() { 368 // There is a bug in Loader which can result in stale data if a loader is stopped 369 // immediately after a call to onContentChanged. As a workaround we stop the 370 // loader before delivering onContentChanged to ensure mContentChanged is set to 371 // true before forceLoad is called. 372 if (isStarted() && !isAbandoned()) { 373 stopLoading() 374 super.onContentChanged() 375 startLoading() 376 } else { 377 super.onContentChanged() 378 } 379 } 380 381 override fun loadInBackground(): Cursor { 382 // Prime the ringtone title cache for later access. Most alarms will refer to 383 // system ringtones. 384 DataModel.dataModel.loadRingtoneTitles() 385 return super.loadInBackground() 386 } 387 } 388 } 389 390 /** 391 * Get alarm by id. 392 * 393 * @param cr provides access to the content model 394 * @param alarmId for the desired alarm. 395 * @return alarm if found, null otherwise 396 */ 397 @JvmStatic getAlarmnull398 fun getAlarm(cr: ContentResolver, alarmId: Long): Alarm? { 399 val cursor: Cursor? = cr.query(getContentUri(alarmId), QUERY_COLUMNS, null, null, null) 400 cursor?.let { 401 if (cursor.moveToFirst()) { 402 return Alarm(cursor) 403 } 404 } 405 406 return null 407 } 408 409 /** 410 * Get all alarms given conditions. 411 * 412 * @param cr provides access to the content model 413 * @param selection A filter declaring which rows to return, formatted as an 414 * SQL WHERE clause (excluding the WHERE itself). Passing null will 415 * return all rows for the given URI. 416 * @param selectionArgs You may include ?s in selection, which will be 417 * replaced by the values from selectionArgs, in the order that they 418 * appear in the selection. The values will be bound as Strings. 419 * @return list of alarms matching where clause or empty list if none found. 420 */ 421 @JvmStatic getAlarmsnull422 fun getAlarms( 423 cr: ContentResolver, 424 selection: String?, 425 vararg selectionArgs: String? 426 ): List<Alarm> { 427 val result: MutableList<Alarm> = LinkedList() 428 val cursor: Cursor? = 429 cr.query(AlarmsColumns.CONTENT_URI, QUERY_COLUMNS, 430 selection, selectionArgs, null) 431 cursor?.let { 432 if (cursor.moveToFirst()) { 433 do { 434 result.add(Alarm(cursor)) 435 } while (cursor.moveToNext()) 436 } 437 } 438 439 return result 440 } 441 442 @JvmStatic isTomorrownull443 fun isTomorrow(alarm: Alarm, now: Calendar): Boolean { 444 if (alarm.instanceState == InstancesColumns.SNOOZE_STATE) { 445 return false 446 } 447 448 val totalAlarmMinutes = alarm.hour * 60 + alarm.minutes 449 val totalNowMinutes = now[Calendar.HOUR_OF_DAY] * 60 + now[Calendar.MINUTE] 450 return totalAlarmMinutes <= totalNowMinutes 451 } 452 453 @JvmStatic addAlarmnull454 fun addAlarm(contentResolver: ContentResolver, alarm: Alarm): Alarm { 455 val values: ContentValues = createContentValues(alarm) 456 val uri: Uri = contentResolver.insert(AlarmsColumns.CONTENT_URI, values)!! 457 alarm.id = getId(uri) 458 return alarm 459 } 460 461 @JvmStatic updateAlarmnull462 fun updateAlarm(contentResolver: ContentResolver, alarm: Alarm): Boolean { 463 if (alarm.id == INVALID_ID) return false 464 val values: ContentValues = createContentValues(alarm) 465 val rowsUpdated: Long = 466 contentResolver.update(getContentUri(alarm.id), values, null, null).toLong() 467 return rowsUpdated == 1L 468 } 469 470 @JvmStatic deleteAlarmnull471 fun deleteAlarm(contentResolver: ContentResolver, alarmId: Long): Boolean { 472 if (alarmId == INVALID_ID) return false 473 val deletedRows: Int = contentResolver.delete(getContentUri(alarmId), "", null) 474 return deletedRows == 1 475 } 476 477 val CREATOR: Parcelable.Creator<Alarm> = object : Parcelable.Creator<Alarm> { createFromParcelnull478 override fun createFromParcel(p: Parcel): Alarm { 479 return Alarm(p) 480 } 481 newArraynull482 override fun newArray(size: Int): Array<Alarm?> { 483 return arrayOfNulls(size) 484 } 485 } 486 } 487 }