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.provider.BaseColumns._ID 28 29 import com.android.deskclock.LogUtils 30 import com.android.deskclock.R 31 import com.android.deskclock.alarms.AlarmStateManager 32 import com.android.deskclock.data.DataModel 33 import com.android.deskclock.provider.ClockContract.AlarmSettingColumns 34 import com.android.deskclock.provider.ClockContract.InstancesColumns 35 36 import java.util.Calendar 37 import java.util.LinkedList 38 39 class AlarmInstance : InstancesColumns { 40 // Public fields 41 var mYear = 0 42 var mMonth = 0 43 var mDay = 0 44 var mHour = 0 45 var mMinute = 0 46 47 @JvmField 48 var mId: Long = 0 49 50 @JvmField 51 var mLabel: String? = null 52 53 @JvmField 54 var mVibrate = false 55 56 @JvmField 57 var mRingtone: Uri? = null 58 59 @JvmField 60 var mAlarmId: Long? = null 61 62 @JvmField 63 var mAlarmState: Int 64 65 constructor(calendar: Calendar, alarmId: Long?) : this(calendar) { 66 mAlarmId = alarmId 67 } 68 69 constructor(calendar: Calendar) { 70 mId = INVALID_ID 71 alarmTime = calendar 72 mLabel = "" 73 mVibrate = false 74 mRingtone = null 75 mAlarmState = InstancesColumns.SILENT_STATE 76 } 77 78 constructor(instance: AlarmInstance) { 79 mId = instance.mId 80 mYear = instance.mYear 81 mMonth = instance.mMonth 82 mDay = instance.mDay 83 mHour = instance.mHour 84 mMinute = instance.mMinute 85 mLabel = instance.mLabel 86 mVibrate = instance.mVibrate 87 mRingtone = instance.mRingtone 88 mAlarmId = instance.mAlarmId 89 mAlarmState = instance.mAlarmState 90 } 91 92 constructor(c: Cursor, joinedTable: Boolean) { 93 if (joinedTable) { 94 mId = c.getLong(Alarm.INSTANCE_ID_INDEX) 95 mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX) 96 mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX) 97 mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX) 98 mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX) 99 mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX) 100 mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX) 101 mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1 102 } else { 103 mId = c.getLong(ID_INDEX) 104 mYear = c.getInt(YEAR_INDEX) 105 mMonth = c.getInt(MONTH_INDEX) 106 mDay = c.getInt(DAY_INDEX) 107 mHour = c.getInt(HOUR_INDEX) 108 mMinute = c.getInt(MINUTES_INDEX) 109 mLabel = c.getString(LABEL_INDEX) 110 mVibrate = c.getInt(VIBRATE_INDEX) == 1 111 } 112 mRingtone = if (c.isNull(RINGTONE_INDEX)) { 113 // Should we be saving this with the current ringtone or leave it null 114 // so it changes when user changes default ringtone? 115 RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) 116 } else { 117 Uri.parse(c.getString(RINGTONE_INDEX)) 118 } 119 120 if (!c.isNull(ALARM_ID_INDEX)) { 121 mAlarmId = c.getLong(ALARM_ID_INDEX) 122 } 123 mAlarmState = c.getInt(ALARM_STATE_INDEX) 124 } 125 126 /** 127 * @return the deeplink that identifies this alarm instance 128 */ 129 val contentUri: Uri 130 get() = getContentUri(mId) 131 getLabelOrDefaultnull132 fun getLabelOrDefault(context: Context): String { 133 return if (mLabel.isNullOrEmpty()) context.getString(R.string.default_label) else mLabel!! 134 } 135 136 /** 137 * Return the time when a alarm should fire. 138 * 139 * @return the time 140 */ 141 var alarmTime: Calendar 142 get() { 143 val calendar = Calendar.getInstance() 144 calendar[Calendar.YEAR] = mYear 145 calendar[Calendar.MONTH] = mMonth 146 calendar[Calendar.DAY_OF_MONTH] = mDay 147 calendar[Calendar.HOUR_OF_DAY] = mHour 148 calendar[Calendar.MINUTE] = mMinute 149 calendar[Calendar.SECOND] = 0 150 calendar[Calendar.MILLISECOND] = 0 151 return calendar 152 } 153 set(calendar) { 154 mYear = calendar[Calendar.YEAR] 155 mMonth = calendar[Calendar.MONTH] 156 mDay = calendar[Calendar.DAY_OF_MONTH] 157 mHour = calendar[Calendar.HOUR_OF_DAY] 158 mMinute = calendar[Calendar.MINUTE] 159 } 160 161 /** 162 * Return the time when a low priority notification should be shown. 163 * 164 * @return the time 165 */ 166 val lowNotificationTime: Calendar 167 get() { 168 val calendar = alarmTime 169 calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET) 170 return calendar 171 } 172 173 /** 174 * Return the time when a high priority notification should be shown. 175 * 176 * @return the time 177 */ 178 val highNotificationTime: Calendar 179 get() { 180 val calendar = alarmTime 181 calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET) 182 return calendar 183 } 184 185 /** 186 * Return the time when a missed notification should be removed. 187 * 188 * @return the time 189 */ 190 val missedTimeToLive: Calendar 191 get() { 192 val calendar = alarmTime 193 calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET) 194 return calendar 195 } 196 197 /** 198 * Return the time when the alarm should stop firing and be marked as missed. 199 * 200 * @return the time when alarm should be silence, or null if never 201 */ 202 val timeout: Calendar? 203 get() { 204 val timeoutMinutes = DataModel.dataModel.alarmTimeout 205 206 // Alarm silence has been set to "None" 207 if (timeoutMinutes < 0) { 208 return null 209 } 210 211 val calendar = alarmTime 212 calendar.add(Calendar.MINUTE, timeoutMinutes) 213 return calendar 214 } 215 equalsnull216 override fun equals(other: Any?): Boolean { 217 if (other !is AlarmInstance) return false 218 return mId == other.mId 219 } 220 hashCodenull221 override fun hashCode(): Int { 222 return java.lang.Long.valueOf(mId).hashCode() 223 } 224 toStringnull225 override fun toString(): String { 226 return "AlarmInstance{" + 227 "mId=" + mId + 228 ", mYear=" + mYear + 229 ", mMonth=" + mMonth + 230 ", mDay=" + mDay + 231 ", mHour=" + mHour + 232 ", mMinute=" + mMinute + 233 ", mLabel=" + mLabel + 234 ", mVibrate=" + mVibrate + 235 ", mRingtone=" + mRingtone + 236 ", mAlarmId=" + mAlarmId + 237 ", mAlarmState=" + mAlarmState + 238 '}' 239 } 240 241 companion object { 242 /** 243 * Offset from alarm time to show low priority notification 244 */ 245 const val LOW_NOTIFICATION_HOUR_OFFSET = -2 246 247 /** 248 * Offset from alarm time to show high priority notification 249 */ 250 const val HIGH_NOTIFICATION_MINUTE_OFFSET = -30 251 252 /** 253 * Offset from alarm time to stop showing missed notification. 254 */ 255 private const val MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12 256 257 /** 258 * AlarmInstances start with an invalid id when it hasn't been saved to the database. 259 */ 260 const val INVALID_ID: Long = -1 261 262 private val QUERY_COLUMNS = arrayOf( 263 _ID, 264 InstancesColumns.YEAR, 265 InstancesColumns.MONTH, 266 InstancesColumns.DAY, 267 InstancesColumns.HOUR, 268 InstancesColumns.MINUTES, 269 AlarmSettingColumns.LABEL, 270 AlarmSettingColumns.VIBRATE, 271 AlarmSettingColumns.RINGTONE, 272 InstancesColumns.ALARM_ID, 273 InstancesColumns.ALARM_STATE 274 ) 275 276 /** 277 * These save calls to cursor.getColumnIndexOrThrow() 278 * THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS 279 */ 280 private const val ID_INDEX = 0 281 private const val YEAR_INDEX = 1 282 private const val MONTH_INDEX = 2 283 private const val DAY_INDEX = 3 284 private const val HOUR_INDEX = 4 285 private const val MINUTES_INDEX = 5 286 private const val LABEL_INDEX = 6 287 private const val VIBRATE_INDEX = 7 288 private const val RINGTONE_INDEX = 8 289 private const val ALARM_ID_INDEX = 9 290 private const val ALARM_STATE_INDEX = 10 291 292 private const val COLUMN_COUNT = ALARM_STATE_INDEX + 1 293 294 @JvmStatic createContentValuesnull295 fun createContentValues(instance: AlarmInstance): ContentValues { 296 val values = ContentValues(COLUMN_COUNT) 297 if (instance.mId != INVALID_ID) { 298 values.put(_ID, instance.mId) 299 } 300 301 values.put(InstancesColumns.YEAR, instance.mYear) 302 values.put(InstancesColumns.MONTH, instance.mMonth) 303 values.put(InstancesColumns.DAY, instance.mDay) 304 values.put(InstancesColumns.HOUR, instance.mHour) 305 values.put(InstancesColumns.MINUTES, instance.mMinute) 306 values.put(AlarmSettingColumns.LABEL, instance.mLabel) 307 values.put(AlarmSettingColumns.VIBRATE, if (instance.mVibrate) 1 else 0) 308 if (instance.mRingtone == null) { 309 // We want to put null in the database, so we'll be able 310 // to pick up on changes to the default alarm 311 values.putNull(AlarmSettingColumns.RINGTONE) 312 } else { 313 values.put(AlarmSettingColumns.RINGTONE, instance.mRingtone.toString()) 314 } 315 values.put(InstancesColumns.ALARM_ID, instance.mAlarmId) 316 values.put(InstancesColumns.ALARM_STATE, instance.mAlarmState) 317 return values 318 } 319 createIntentnull320 fun createIntent(action: String?, instanceId: Long): Intent { 321 return Intent(action).setData(getContentUri(instanceId)) 322 } 323 324 @JvmStatic createIntentnull325 fun createIntent(context: Context?, cls: Class<*>?, instanceId: Long): Intent { 326 return Intent(context, cls).setData(getContentUri(instanceId)) 327 } 328 329 @JvmStatic getIdnull330 fun getId(contentUri: Uri): Long { 331 return ContentUris.parseId(contentUri) 332 } 333 334 /** 335 * @return the [Uri] identifying the alarm instance 336 */ getContentUrinull337 fun getContentUri(instanceId: Long): Uri { 338 return ContentUris.withAppendedId(InstancesColumns.CONTENT_URI, instanceId) 339 } 340 341 /** 342 * Get alarm instance from instanceId. 343 * 344 * @param cr provides access to the content model 345 * @param instanceId for the desired instance. 346 * @return instance if found, null otherwise 347 */ 348 @JvmStatic getInstancenull349 fun getInstance(cr: ContentResolver, instanceId: Long): AlarmInstance? { 350 val cursor: Cursor? = 351 cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null) 352 cursor?.let { 353 if (cursor.moveToFirst()) { 354 return AlarmInstance(cursor, false /* joinedTable */) 355 } 356 } 357 return null 358 } 359 360 /** 361 * Get alarm instance for the `contentUri`. 362 * 363 * @param cr provides access to the content model 364 * @param contentUri the [deeplink][.getContentUri] for the desired instance 365 * @return instance if found, null otherwise 366 */ getInstancenull367 fun getInstance(cr: ContentResolver, contentUri: Uri): AlarmInstance? { 368 val instanceId: Long = ContentUris.parseId(contentUri) 369 return getInstance(cr, instanceId) 370 } 371 372 /** 373 * Get an alarm instances by alarmId. 374 * 375 * @param contentResolver provides access to the content model 376 * @param alarmId of instances desired. 377 * @return list of alarms instances that are owned by alarmId. 378 */ 379 @JvmStatic getInstancesByAlarmIdnull380 fun getInstancesByAlarmId( 381 contentResolver: ContentResolver, 382 alarmId: Long 383 ): List<AlarmInstance> { 384 return getInstances(contentResolver, InstancesColumns.ALARM_ID + "=" + alarmId) 385 } 386 387 /** 388 * Get the next instance of an alarm given its alarmId 389 * @param contentResolver provides access to the content model 390 * @param alarmId of instance desired 391 * @return the next instance of an alarm by alarmId. 392 */ 393 @JvmStatic getNextUpcomingInstanceByAlarmIdnull394 fun getNextUpcomingInstanceByAlarmId( 395 contentResolver: ContentResolver, 396 alarmId: Long 397 ): AlarmInstance? { 398 val alarmInstances = getInstancesByAlarmId(contentResolver, alarmId) 399 if (alarmInstances.isEmpty()) { 400 return null 401 } 402 var nextAlarmInstance = alarmInstances[0] 403 for (instance in alarmInstances) { 404 if (instance.alarmTime.before(nextAlarmInstance.alarmTime)) { 405 nextAlarmInstance = instance 406 } 407 } 408 return nextAlarmInstance 409 } 410 411 /** 412 * Get alarm instance by id and state. 413 */ getInstancesByInstanceIdAndStatenull414 fun getInstancesByInstanceIdAndState( 415 contentResolver: ContentResolver, 416 alarmInstanceId: Long, 417 state: Int 418 ): List<AlarmInstance> { 419 return getInstances(contentResolver, 420 _ID.toString() + "=" + alarmInstanceId + " AND " + 421 InstancesColumns.ALARM_STATE + "=" + state) 422 } 423 424 /** 425 * Get alarm instances in the specified state. 426 */ 427 @JvmStatic getInstancesByStatenull428 fun getInstancesByState( 429 contentResolver: ContentResolver, 430 state: Int 431 ): List<AlarmInstance> { 432 return getInstances(contentResolver, 433 InstancesColumns.ALARM_STATE + "=" + state) 434 } 435 436 /** 437 * Get a list of instances given selection. 438 * 439 * @param cr provides access to the content model 440 * @param selection A filter declaring which rows to return, formatted as an 441 * SQL WHERE clause (excluding the WHERE itself). Passing null will 442 * return all rows for the given URI. 443 * @param selectionArgs You may include ?s in selection, which will be 444 * replaced by the values from selectionArgs, in the order that they 445 * appear in the selection. The values will be bound as Strings. 446 * @return list of alarms matching where clause or empty list if none found. 447 */ 448 @JvmStatic getInstancesnull449 fun getInstances( 450 cr: ContentResolver, 451 selection: String?, 452 vararg selectionArgs: String? 453 ): MutableList<AlarmInstance> { 454 val result: MutableList<AlarmInstance> = LinkedList() 455 val cursor: Cursor? = 456 cr.query(InstancesColumns.CONTENT_URI, QUERY_COLUMNS, 457 selection, selectionArgs, null) 458 cursor?.let { 459 if (cursor.moveToFirst()) { 460 do { 461 result.add(AlarmInstance(cursor, false /* joinedTable */)) 462 } while (cursor.moveToNext()) 463 } 464 } 465 466 return result 467 } 468 469 @JvmStatic addInstancenull470 fun addInstance( 471 contentResolver: ContentResolver, 472 instance: AlarmInstance 473 ): AlarmInstance { 474 // Make sure we are not adding a duplicate instances. This is not a 475 // fix and should never happen. This is only a safe guard against bad code, and you 476 // should fix the root issue if you see the error message. 477 val dupSelector = InstancesColumns.ALARM_ID + " = " + instance.mAlarmId 478 for (otherInstances in getInstances(contentResolver, dupSelector)) { 479 if (otherInstances.alarmTime == instance.alarmTime) { 480 LogUtils.i("Detected duplicate instance in DB. Updating " + 481 otherInstances + " to " + instance) 482 // Copy over the new instance values and update the db 483 instance.mId = otherInstances.mId 484 updateInstance(contentResolver, instance) 485 return instance 486 } 487 } 488 489 val values: ContentValues = createContentValues(instance) 490 val uri: Uri = contentResolver.insert(InstancesColumns.CONTENT_URI, values)!! 491 instance.mId = getId(uri) 492 return instance 493 } 494 495 @JvmStatic updateInstancenull496 fun updateInstance(contentResolver: ContentResolver, instance: AlarmInstance): Boolean { 497 if (instance.mId == INVALID_ID) return false 498 val values: ContentValues = createContentValues(instance) 499 val rowsUpdated: Long = 500 contentResolver.update(getContentUri(instance.mId), values, null, null).toLong() 501 return rowsUpdated == 1L 502 } 503 504 @JvmStatic deleteInstancenull505 fun deleteInstance(contentResolver: ContentResolver, instanceId: Long): Boolean { 506 if (instanceId == INVALID_ID) return false 507 val deletedRows: Int = contentResolver.delete(getContentUri(instanceId), "", null) 508 return deletedRows == 1 509 } 510 511 @JvmStatic deleteOtherInstancesnull512 fun deleteOtherInstances( 513 context: Context, 514 contentResolver: ContentResolver, 515 alarmId: Long, 516 instanceId: Long 517 ) { 518 val instances = getInstancesByAlarmId(contentResolver, alarmId) 519 for (instance in instances) { 520 if (instance.mId != instanceId) { 521 AlarmStateManager.unregisterInstance(context, instance) 522 deleteInstance(contentResolver, instance.mId) 523 } 524 } 525 } 526 } 527 }