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 }