/* * 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.dataModel.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 { 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 { 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 { 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 { val result: MutableList = 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) } } } } }