• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }