Home | History | Annotate | Download | only in provider
      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 {
    116         sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms", ALARMS);
    117         sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms/#", ALARMS_ID);
    118         sURIMatcher.addURI(ClockContract.AUTHORITY, "instances", INSTANCES);
    119         sURIMatcher.addURI(ClockContract.AUTHORITY, "instances/#", INSTANCES_ID);
    120         sURIMatcher.addURI(ClockContract.AUTHORITY, "alarms_with_instances", ALARMS_WITH_INSTANCES);
    121     }
    122 
    123     public ClockProvider() {
    124     }
    125 
    126     @Override
    127     @TargetApi(Build.VERSION_CODES.N)
    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
    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
    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
    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
    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
    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      */
    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