• 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.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 }