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