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 }