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.annotation.TargetApi
20 import android.content.ContentProvider
21 import android.content.ContentResolver
22 import android.content.ContentUris
23 import android.content.ContentValues
24 import android.content.Context
25 import android.content.UriMatcher
26 import android.database.Cursor
27 import android.database.sqlite.SQLiteDatabase
28 import android.database.sqlite.SQLiteQueryBuilder
29 import android.net.Uri
30 import android.os.Build
31 import android.provider.BaseColumns
32 import android.text.TextUtils
33 import android.util.ArrayMap
34 
35 import com.android.deskclock.LogUtils
36 import com.android.deskclock.Utils
37 import com.android.deskclock.provider.ClockContract.AlarmSettingColumns
38 import com.android.deskclock.provider.ClockContract.AlarmsColumns
39 import com.android.deskclock.provider.ClockContract.InstancesColumns
40 import com.android.deskclock.provider.ClockDatabaseHelper.Companion.ALARMS_TABLE_NAME
41 import com.android.deskclock.provider.ClockDatabaseHelper.Companion.INSTANCES_TABLE_NAME
42 
43 class ClockProvider : ContentProvider() {
44 
45     private lateinit var mOpenHelper: ClockDatabaseHelper
46 
47     companion object {
48         private const val ALARMS = 1
49         private const val ALARMS_ID = 2
50         private const val INSTANCES = 3
51         private const val INSTANCES_ID = 4
52         private const val ALARMS_WITH_INSTANCES = 5
53 
54         private val ALARM_JOIN_INSTANCE_TABLE_STATEMENT =
55                 ALARMS_TABLE_NAME + " LEFT JOIN " +
56                         INSTANCES_TABLE_NAME + " ON (" +
57                         ALARMS_TABLE_NAME + "." +
58                         BaseColumns._ID + " = " + InstancesColumns.ALARM_ID + ")"
59 
60         private val ALARM_JOIN_INSTANCE_WHERE_STATEMENT = INSTANCES_TABLE_NAME +
61                 "." + BaseColumns._ID + " IS NULL OR " +
62                 INSTANCES_TABLE_NAME + "." + BaseColumns._ID + " = (" +
63                 "SELECT " + BaseColumns._ID +
64                 " FROM " + INSTANCES_TABLE_NAME +
65                 " WHERE " + InstancesColumns.ALARM_ID +
66                 " = " + ALARMS_TABLE_NAME + "." + BaseColumns._ID +
67                 " ORDER BY " + InstancesColumns.ALARM_STATE + ", " +
68                 InstancesColumns.YEAR + ", " + InstancesColumns.MONTH + ", " +
69                 InstancesColumns.DAY + " LIMIT 1)"
70 
71         /**
72          * Projection map used by query for snoozed alarms.
73          */
74         private val sAlarmsWithInstancesProjection: MutableMap<String, String> = ArrayMap()
75 
76         private val sURIMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
77 
78         init {
79             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + BaseColumns._ID] =
80                     ALARMS_TABLE_NAME + "." + BaseColumns._ID
81             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR] =
82                     ALARMS_TABLE_NAME + "." + AlarmsColumns.HOUR
83             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES] =
84                     ALARMS_TABLE_NAME + "." + AlarmsColumns.MINUTES
85             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK] =
86                     ALARMS_TABLE_NAME + "." + AlarmsColumns.DAYS_OF_WEEK
87             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED] =
88                     ALARMS_TABLE_NAME + "." + AlarmsColumns.ENABLED
89             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE] =
90                     ALARMS_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
91             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
92                     ALARMS_TABLE_NAME + "." + AlarmSettingColumns.LABEL
93             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE] =
94                     ALARMS_TABLE_NAME + "." + AlarmSettingColumns.RINGTONE
95             sAlarmsWithInstancesProjection[ALARMS_TABLE_NAME + "." +
96                     AlarmsColumns.DELETE_AFTER_USE] =
97                     ALARMS_TABLE_NAME + "." + AlarmsColumns.DELETE_AFTER_USE
98             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
99                     InstancesColumns.ALARM_STATE] =
100                     INSTANCES_TABLE_NAME + "." + InstancesColumns.ALARM_STATE
101             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + BaseColumns._ID] =
102                     INSTANCES_TABLE_NAME + "." + BaseColumns._ID
103             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR] =
104                     INSTANCES_TABLE_NAME + "." + InstancesColumns.YEAR
105             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH] =
106                     INSTANCES_TABLE_NAME + "." + InstancesColumns.MONTH
107             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY] =
108                     INSTANCES_TABLE_NAME + "." + InstancesColumns.DAY
109             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR] =
110                     INSTANCES_TABLE_NAME + "." + InstancesColumns.HOUR
111             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES] =
112                     INSTANCES_TABLE_NAME + "." + InstancesColumns.MINUTES
113             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL] =
114                     INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.LABEL
115             sAlarmsWithInstancesProjection[INSTANCES_TABLE_NAME + "." +
116                     AlarmSettingColumns.VIBRATE] =
117                     INSTANCES_TABLE_NAME + "." + AlarmSettingColumns.VIBRATE
118 
119             sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS)
120             sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID)
121             sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES)
122             sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID)
123             sURIMatcher.addURI(ClockContract.AUTHORITY,
124                     "alarms_with_instances", ALARMS_WITH_INSTANCES)
125         }
126     }
127 
128     @TargetApi(Build.VERSION_CODES.N)
onCreatenull129     override fun onCreate(): Boolean {
130         val context: Context = getContext()!!
131         val storageContext: Context
132         if (Utils.isNOrLater) {
133             // All N devices have split storage areas, but we may need to
134             // migrate existing database into the new device encrypted
135             // storage area, which is where our data lives from now on.
136             storageContext = context.createDeviceProtectedStorageContext()
137             if (!storageContext.moveDatabaseFrom(context, ClockDatabaseHelper.DATABASE_NAME)) {
138                 LogUtils.wtf("Failed to migrate database: %s",
139                         ClockDatabaseHelper.DATABASE_NAME)
140             }
141         } else {
142             storageContext = context
143         }
144 
145         mOpenHelper = ClockDatabaseHelper(storageContext)
146         return true
147     }
148 
querynull149     override fun query(
150         uri: Uri,
151         projectionIn: Array<String?>?,
152         selection: String?,
153         selectionArgs: Array<String?>?,
154         sort: String?
155     ): Cursor? {
156         val qb = SQLiteQueryBuilder()
157         val db: SQLiteDatabase = mOpenHelper.getReadableDatabase()
158 
159         // Generate the body of the query
160         when (sURIMatcher.match(uri)) {
161             ALARMS -> qb.setTables(ALARMS_TABLE_NAME)
162             ALARMS_ID -> {
163                 qb.setTables(ALARMS_TABLE_NAME)
164                 qb.appendWhere(BaseColumns._ID.toString() + "=")
165                 qb.appendWhere(uri.getLastPathSegment()!!)
166             }
167             INSTANCES -> qb.setTables(INSTANCES_TABLE_NAME)
168             INSTANCES_ID -> {
169                 qb.setTables(INSTANCES_TABLE_NAME)
170                 qb.appendWhere(BaseColumns._ID.toString() + "=")
171                 qb.appendWhere(uri.getLastPathSegment()!!)
172             }
173             ALARMS_WITH_INSTANCES -> {
174                 qb.setTables(ALARM_JOIN_INSTANCE_TABLE_STATEMENT)
175                 qb.appendWhere(ALARM_JOIN_INSTANCE_WHERE_STATEMENT)
176                 qb.setProjectionMap(sAlarmsWithInstancesProjection)
177             }
178             else -> throw IllegalArgumentException("Unknown URI $uri")
179         }
180 
181         val ret: Cursor? = qb.query(db, projectionIn, selection, selectionArgs, null, null, sort)
182         if (ret == null) {
183             LogUtils.e("Alarms.query: failed")
184         } else {
185             ret.setNotificationUri(getContext()!!.getContentResolver(), uri)
186         }
187 
188         return ret
189     }
190 
getTypenull191     override fun getType(uri: Uri): String {
192         return when (sURIMatcher.match(uri)) {
193             ALARMS -> "vnd.android.cursor.dir/alarms"
194             ALARMS_ID -> "vnd.android.cursor.item/alarms"
195             INSTANCES -> "vnd.android.cursor.dir/instances"
196             INSTANCES_ID -> "vnd.android.cursor.item/instances"
197             else -> throw IllegalArgumentException("Unknown URI")
198         }
199     }
200 
updatenull201     override fun update(
202         uri: Uri,
203         values: ContentValues?,
204         where: String?,
205         whereArgs: Array<String?>?
206     ): Int {
207         val count: Int
208         val alarmId: String?
209         val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
210         when (sURIMatcher.match(uri)) {
211             ALARMS_ID -> {
212                 alarmId = uri.getLastPathSegment()
213                 count = db.update(ALARMS_TABLE_NAME, values,
214                         BaseColumns._ID.toString() + "=" + alarmId,
215                         null)
216             }
217             INSTANCES_ID -> {
218                 alarmId = uri.getLastPathSegment()
219                 count = db.update(INSTANCES_TABLE_NAME, values,
220                         BaseColumns._ID.toString() + "=" + alarmId,
221                         null)
222             }
223             else -> {
224                 throw UnsupportedOperationException("Cannot update URI: $uri")
225             }
226         }
227         LogUtils.v("*** notifyChange() id: $alarmId url $uri")
228         notifyChange(getContext()!!.getContentResolver(), uri)
229         return count
230     }
231 
insertnull232     override fun insert(uri: Uri, initialValues: ContentValues?): Uri? {
233         val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
234         val rowId: Long = when (sURIMatcher.match(uri)) {
235             ALARMS -> mOpenHelper.fixAlarmInsert(initialValues!!)
236             INSTANCES -> db.insert(INSTANCES_TABLE_NAME, null, initialValues)
237             else -> throw IllegalArgumentException("Cannot insert from URI: $uri")
238         }
239 
240         val uriResult: Uri = ContentUris.withAppendedId(uri, rowId)
241         notifyChange(getContext()!!.getContentResolver(), uriResult)
242         return uriResult
243     }
244 
deletenull245     override fun delete(uri: Uri, where: String?, whereArgs: Array<String>?): Int {
246         var whereString = where
247         val count: Int
248         val primaryKey: String?
249         val db: SQLiteDatabase = mOpenHelper.getWritableDatabase()
250         when (sURIMatcher.match(uri)) {
251             ALARMS -> count =
252                     db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
253             ALARMS_ID -> {
254                 primaryKey = uri.getLastPathSegment()
255                 whereString = if (TextUtils.isEmpty(whereString)) {
256                     BaseColumns._ID.toString() + "=" + primaryKey
257                 } else {
258                     BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
259                 }
260                 count = db.delete(ALARMS_TABLE_NAME, whereString, whereArgs)
261             }
262             INSTANCES -> count =
263                     db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
264             INSTANCES_ID -> {
265                 primaryKey = uri.getLastPathSegment()
266                 whereString = if (TextUtils.isEmpty(whereString)) {
267                     BaseColumns._ID.toString() + "=" + primaryKey
268                 } else {
269                     BaseColumns._ID.toString() + "=" + primaryKey + " AND (" + whereString + ")"
270                 }
271                 count = db.delete(INSTANCES_TABLE_NAME, whereString, whereArgs)
272             }
273             else -> throw IllegalArgumentException("Cannot delete from URI: $uri")
274         }
275 
276         notifyChange(getContext()!!.getContentResolver(), uri)
277         return count
278     }
279 
280     /**
281      * Notify affected URIs of changes.
282      */
notifyChangenull283     private fun notifyChange(resolver: ContentResolver, uri: Uri) {
284         resolver.notifyChange(uri, null)
285 
286         val match: Int = sURIMatcher.match(uri)
287         // Also notify the joined table of changes to instances or alarms.
288         if (match == ALARMS || match == INSTANCES || match == ALARMS_ID || match == INSTANCES_ID) {
289             resolver.notifyChange(AlarmsColumns.ALARMS_WITH_INSTANCES_URI, null)
290         }
291     }
292 }