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