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