blob: 810930ca63e407f6dccd357e10a104733150577c [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.deskclock.provider
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.media.RingtoneManager
import android.net.Uri
import android.provider.BaseColumns._ID
import com.android.deskclock.LogUtils
import com.android.deskclock.R
import com.android.deskclock.alarms.AlarmStateManager
import com.android.deskclock.data.DataModel
import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
import com.android.deskclock.provider.ClockContract.InstancesColumns
import java.util.Calendar
import java.util.LinkedList
class AlarmInstance : InstancesColumns {
// Public fields
var mYear = 0
var mMonth = 0
var mDay = 0
var mHour = 0
var mMinute = 0
@JvmField
var mId: Long = 0
@JvmField
var mLabel: String? = null
@JvmField
var mVibrate = false
@JvmField
var mRingtone: Uri? = null
@JvmField
var mAlarmId: Long? = null
@JvmField
var mAlarmState: Int
constructor(calendar: Calendar, alarmId: Long?) : this(calendar) {
mAlarmId = alarmId
}
constructor(calendar: Calendar) {
mId = INVALID_ID
alarmTime = calendar
mLabel = ""
mVibrate = false
mRingtone = null
mAlarmState = InstancesColumns.SILENT_STATE
}
constructor(instance: AlarmInstance) {
mId = instance.mId
mYear = instance.mYear
mMonth = instance.mMonth
mDay = instance.mDay
mHour = instance.mHour
mMinute = instance.mMinute
mLabel = instance.mLabel
mVibrate = instance.mVibrate
mRingtone = instance.mRingtone
mAlarmId = instance.mAlarmId
mAlarmState = instance.mAlarmState
}
constructor(c: Cursor, joinedTable: Boolean) {
if (joinedTable) {
mId = c.getLong(Alarm.INSTANCE_ID_INDEX)
mYear = c.getInt(Alarm.INSTANCE_YEAR_INDEX)
mMonth = c.getInt(Alarm.INSTANCE_MONTH_INDEX)
mDay = c.getInt(Alarm.INSTANCE_DAY_INDEX)
mHour = c.getInt(Alarm.INSTANCE_HOUR_INDEX)
mMinute = c.getInt(Alarm.INSTANCE_MINUTE_INDEX)
mLabel = c.getString(Alarm.INSTANCE_LABEL_INDEX)
mVibrate = c.getInt(Alarm.INSTANCE_VIBRATE_INDEX) == 1
} else {
mId = c.getLong(ID_INDEX)
mYear = c.getInt(YEAR_INDEX)
mMonth = c.getInt(MONTH_INDEX)
mDay = c.getInt(DAY_INDEX)
mHour = c.getInt(HOUR_INDEX)
mMinute = c.getInt(MINUTES_INDEX)
mLabel = c.getString(LABEL_INDEX)
mVibrate = c.getInt(VIBRATE_INDEX) == 1
}
mRingtone = if (c.isNull(RINGTONE_INDEX)) {
// Should we be saving this with the current ringtone or leave it null
// so it changes when user changes default ringtone?
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
} else {
Uri.parse(c.getString(RINGTONE_INDEX))
}
if (!c.isNull(ALARM_ID_INDEX)) {
mAlarmId = c.getLong(ALARM_ID_INDEX)
}
mAlarmState = c.getInt(ALARM_STATE_INDEX)
}
/**
* @return the deeplink that identifies this alarm instance
*/
val contentUri: Uri
get() = getContentUri(mId)
fun getLabelOrDefault(context: Context): String {
return if (mLabel.isNullOrEmpty()) context.getString(R.string.default_label) else mLabel!!
}
/**
* Return the time when a alarm should fire.
*
* @return the time
*/
var alarmTime: Calendar
get() {
val calendar = Calendar.getInstance()
calendar[Calendar.YEAR] = mYear
calendar[Calendar.MONTH] = mMonth
calendar[Calendar.DAY_OF_MONTH] = mDay
calendar[Calendar.HOUR_OF_DAY] = mHour
calendar[Calendar.MINUTE] = mMinute
calendar[Calendar.SECOND] = 0
calendar[Calendar.MILLISECOND] = 0
return calendar
}
set(calendar) {
mYear = calendar[Calendar.YEAR]
mMonth = calendar[Calendar.MONTH]
mDay = calendar[Calendar.DAY_OF_MONTH]
mHour = calendar[Calendar.HOUR_OF_DAY]
mMinute = calendar[Calendar.MINUTE]
}
/**
* Return the time when a low priority notification should be shown.
*
* @return the time
*/
val lowNotificationTime: Calendar
get() {
val calendar = alarmTime
calendar.add(Calendar.HOUR_OF_DAY, LOW_NOTIFICATION_HOUR_OFFSET)
return calendar
}
/**
* Return the time when a high priority notification should be shown.
*
* @return the time
*/
val highNotificationTime: Calendar
get() {
val calendar = alarmTime
calendar.add(Calendar.MINUTE, HIGH_NOTIFICATION_MINUTE_OFFSET)
return calendar
}
/**
* Return the time when a missed notification should be removed.
*
* @return the time
*/
val missedTimeToLive: Calendar
get() {
val calendar = alarmTime
calendar.add(Calendar.HOUR, MISSED_TIME_TO_LIVE_HOUR_OFFSET)
return calendar
}
/**
* Return the time when the alarm should stop firing and be marked as missed.
*
* @return the time when alarm should be silence, or null if never
*/
val timeout: Calendar?
get() {
val timeoutMinutes = DataModel.getDataModel().alarmTimeout
// Alarm silence has been set to "None"
if (timeoutMinutes < 0) {
return null
}
val calendar = alarmTime
calendar.add(Calendar.MINUTE, timeoutMinutes)
return calendar
}
override fun equals(other: Any?): Boolean {
if (other !is AlarmInstance) return false
return mId == other.mId
}
override fun hashCode(): Int {
return java.lang.Long.valueOf(mId).hashCode()
}
override fun toString(): String {
return "AlarmInstance{" +
"mId=" + mId +
", mYear=" + mYear +
", mMonth=" + mMonth +
", mDay=" + mDay +
", mHour=" + mHour +
", mMinute=" + mMinute +
", mLabel=" + mLabel +
", mVibrate=" + mVibrate +
", mRingtone=" + mRingtone +
", mAlarmId=" + mAlarmId +
", mAlarmState=" + mAlarmState +
'}'
}
companion object {
/**
* Offset from alarm time to show low priority notification
*/
const val LOW_NOTIFICATION_HOUR_OFFSET = -2
/**
* Offset from alarm time to show high priority notification
*/
const val HIGH_NOTIFICATION_MINUTE_OFFSET = -30
/**
* Offset from alarm time to stop showing missed notification.
*/
private const val MISSED_TIME_TO_LIVE_HOUR_OFFSET = 12
/**
* AlarmInstances start with an invalid id when it hasn't been saved to the database.
*/
const val INVALID_ID: Long = -1
private val QUERY_COLUMNS = arrayOf(
_ID,
InstancesColumns.YEAR,
InstancesColumns.MONTH,
InstancesColumns.DAY,
InstancesColumns.HOUR,
InstancesColumns.MINUTES,
AlarmSettingColumns.LABEL,
AlarmSettingColumns.VIBRATE,
AlarmSettingColumns.RINGTONE,
InstancesColumns.ALARM_ID,
InstancesColumns.ALARM_STATE
)
/**
* These save calls to cursor.getColumnIndexOrThrow()
* THEY MUST BE KEPT IN SYNC WITH ABOVE QUERY COLUMNS
*/
private const val ID_INDEX = 0
private const val YEAR_INDEX = 1
private const val MONTH_INDEX = 2
private const val DAY_INDEX = 3
private const val HOUR_INDEX = 4
private const val MINUTES_INDEX = 5
private const val LABEL_INDEX = 6
private const val VIBRATE_INDEX = 7
private const val RINGTONE_INDEX = 8
private const val ALARM_ID_INDEX = 9
private const val ALARM_STATE_INDEX = 10
private const val COLUMN_COUNT = ALARM_STATE_INDEX + 1
@JvmStatic
fun createContentValues(instance: AlarmInstance): ContentValues {
val values = ContentValues(COLUMN_COUNT)
if (instance.mId != INVALID_ID) {
values.put(_ID, instance.mId)
}
values.put(InstancesColumns.YEAR, instance.mYear)
values.put(InstancesColumns.MONTH, instance.mMonth)
values.put(InstancesColumns.DAY, instance.mDay)
values.put(InstancesColumns.HOUR, instance.mHour)
values.put(InstancesColumns.MINUTES, instance.mMinute)
values.put(AlarmSettingColumns.LABEL, instance.mLabel)
values.put(AlarmSettingColumns.VIBRATE, if (instance.mVibrate) 1 else 0)
if (instance.mRingtone == null) {
// We want to put null in the database, so we'll be able
// to pick up on changes to the default alarm
values.putNull(AlarmSettingColumns.RINGTONE)
} else {
values.put(AlarmSettingColumns.RINGTONE, instance.mRingtone.toString())
}
values.put(InstancesColumns.ALARM_ID, instance.mAlarmId)
values.put(InstancesColumns.ALARM_STATE, instance.mAlarmState)
return values
}
fun createIntent(action: String?, instanceId: Long): Intent {
return Intent(action).setData(getContentUri(instanceId))
}
@JvmStatic
fun createIntent(context: Context?, cls: Class<*>?, instanceId: Long): Intent {
return Intent(context, cls).setData(getContentUri(instanceId))
}
@JvmStatic
fun getId(contentUri: Uri): Long {
return ContentUris.parseId(contentUri)
}
/**
* @return the [Uri] identifying the alarm instance
*/
fun getContentUri(instanceId: Long): Uri {
return ContentUris.withAppendedId(InstancesColumns.CONTENT_URI, instanceId)
}
/**
* Get alarm instance from instanceId.
*
* @param cr provides access to the content model
* @param instanceId for the desired instance.
* @return instance if found, null otherwise
*/
@JvmStatic
fun getInstance(cr: ContentResolver, instanceId: Long): AlarmInstance? {
val cursor: Cursor? =
cr.query(getContentUri(instanceId), QUERY_COLUMNS, null, null, null)
cursor?.let {
if (cursor.moveToFirst()) {
return AlarmInstance(cursor, false /* joinedTable */)
}
}
return null
}
/**
* Get alarm instance for the `contentUri`.
*
* @param cr provides access to the content model
* @param contentUri the [deeplink][.getContentUri] for the desired instance
* @return instance if found, null otherwise
*/
fun getInstance(cr: ContentResolver, contentUri: Uri): AlarmInstance? {
val instanceId: Long = ContentUris.parseId(contentUri)
return getInstance(cr, instanceId)
}
/**
* Get an alarm instances by alarmId.
*
* @param contentResolver provides access to the content model
* @param alarmId of instances desired.
* @return list of alarms instances that are owned by alarmId.
*/
@JvmStatic
fun getInstancesByAlarmId(
contentResolver: ContentResolver,
alarmId: Long
): List<AlarmInstance> {
return getInstances(contentResolver, InstancesColumns.ALARM_ID + "=" + alarmId)
}
/**
* Get the next instance of an alarm given its alarmId
* @param contentResolver provides access to the content model
* @param alarmId of instance desired
* @return the next instance of an alarm by alarmId.
*/
@JvmStatic
fun getNextUpcomingInstanceByAlarmId(
contentResolver: ContentResolver,
alarmId: Long
): AlarmInstance? {
val alarmInstances = getInstancesByAlarmId(contentResolver, alarmId)
if (alarmInstances.isEmpty()) {
return null
}
var nextAlarmInstance = alarmInstances[0]
for (instance in alarmInstances) {
if (instance.alarmTime.before(nextAlarmInstance.alarmTime)) {
nextAlarmInstance = instance
}
}
return nextAlarmInstance
}
/**
* Get alarm instance by id and state.
*/
fun getInstancesByInstanceIdAndState(
contentResolver: ContentResolver,
alarmInstanceId: Long,
state: Int
): List<AlarmInstance> {
return getInstances(contentResolver,
_ID.toString() + "=" + alarmInstanceId + " AND " +
InstancesColumns.ALARM_STATE + "=" + state)
}
/**
* Get alarm instances in the specified state.
*/
@JvmStatic
fun getInstancesByState(
contentResolver: ContentResolver,
state: Int
): List<AlarmInstance> {
return getInstances(contentResolver,
InstancesColumns.ALARM_STATE + "=" + state)
}
/**
* Get a list of instances given selection.
*
* @param cr provides access to the content model
* @param selection A filter declaring which rows to return, formatted as an
* SQL WHERE clause (excluding the WHERE itself). Passing null will
* return all rows for the given URI.
* @param selectionArgs You may include ?s in selection, which will be
* replaced by the values from selectionArgs, in the order that they
* appear in the selection. The values will be bound as Strings.
* @return list of alarms matching where clause or empty list if none found.
*/
@JvmStatic
fun getInstances(
cr: ContentResolver,
selection: String?,
vararg selectionArgs: String?
): MutableList<AlarmInstance> {
val result: MutableList<AlarmInstance> = LinkedList()
val cursor: Cursor? =
cr.query(InstancesColumns.CONTENT_URI, QUERY_COLUMNS,
selection, selectionArgs, null)
cursor?.let {
if (cursor.moveToFirst()) {
do {
result.add(AlarmInstance(cursor, false /* joinedTable */))
} while (cursor.moveToNext())
}
}
return result
}
@JvmStatic
fun addInstance(
contentResolver: ContentResolver,
instance: AlarmInstance
): AlarmInstance {
// Make sure we are not adding a duplicate instances. This is not a
// fix and should never happen. This is only a safe guard against bad code, and you
// should fix the root issue if you see the error message.
val dupSelector = InstancesColumns.ALARM_ID + " = " + instance.mAlarmId
for (otherInstances in getInstances(contentResolver, dupSelector)) {
if (otherInstances.alarmTime == instance.alarmTime) {
LogUtils.i("Detected duplicate instance in DB. Updating " +
otherInstances + " to " + instance)
// Copy over the new instance values and update the db
instance.mId = otherInstances.mId
updateInstance(contentResolver, instance)
return instance
}
}
val values: ContentValues = createContentValues(instance)
val uri: Uri = contentResolver.insert(InstancesColumns.CONTENT_URI, values)!!
instance.mId = getId(uri)
return instance
}
@JvmStatic
fun updateInstance(contentResolver: ContentResolver, instance: AlarmInstance): Boolean {
if (instance.mId == INVALID_ID) return false
val values: ContentValues = createContentValues(instance)
val rowsUpdated: Long =
contentResolver.update(getContentUri(instance.mId), values, null, null).toLong()
return rowsUpdated == 1L
}
@JvmStatic
fun deleteInstance(contentResolver: ContentResolver, instanceId: Long): Boolean {
if (instanceId == INVALID_ID) return false
val deletedRows: Int = contentResolver.delete(getContentUri(instanceId), "", null)
return deletedRows == 1
}
@JvmStatic
fun deleteOtherInstances(
context: Context,
contentResolver: ContentResolver,
alarmId: Long,
instanceId: Long
) {
val instances = getInstancesByAlarmId(contentResolver, alarmId)
for (instance in instances) {
if (instance.mId != instanceId) {
AlarmStateManager.unregisterInstance(context, instance)
deleteInstance(contentResolver, instance.mId)
}
}
}
}
}