1 /*
2  * Copyright (C) 2012 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.example.android.threadsample;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.UriMatcher;
23 import android.database.Cursor;
24 import android.database.sqlite.SQLiteDatabase;
25 import android.database.sqlite.SQLiteException;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.net.Uri;
28 import android.util.Log;
29 import android.util.SparseArray;
30 
31 /**
32  *
33  * Defines a ContentProvider that stores URLs of Picasa featured pictures
34  * The provider also has a table that tracks the last time a picture URL was updated.
35  */
36 public class DataProvider extends ContentProvider {
37     // Indicates that the incoming query is for a picture URL
38     public static final int IMAGE_URL_QUERY = 1;
39 
40     // Indicates that the incoming query is for a URL modification date
41     public static final int URL_DATE_QUERY = 2;
42 
43     // Indicates an invalid content URI
44     public static final int INVALID_URI = -1;
45 
46     // Constants for building SQLite tables during initialization
47     private static final String TEXT_TYPE = "TEXT";
48     private static final String PRIMARY_KEY_TYPE = "INTEGER PRIMARY KEY";
49     private static final String INTEGER_TYPE = "INTEGER";
50 
51     // Defines an SQLite statement that builds the Picasa picture URL table
52     private static final String CREATE_PICTUREURL_TABLE_SQL = "CREATE TABLE" + " " +
53             DataProviderContract.PICTUREURL_TABLE_NAME + " " +
54             "(" + " " +
55             DataProviderContract.ROW_ID + " " + PRIMARY_KEY_TYPE + " ," +
56             DataProviderContract.IMAGE_THUMBURL_COLUMN + " " + TEXT_TYPE + " ," +
57             DataProviderContract.IMAGE_URL_COLUMN + " " + TEXT_TYPE + " ," +
58             DataProviderContract.IMAGE_THUMBNAME_COLUMN + " " + TEXT_TYPE + " ," +
59             DataProviderContract.IMAGE_PICTURENAME_COLUMN + " " + TEXT_TYPE +
60             ")";
61 
62     // Defines an SQLite statement that builds the URL modification date table
63     private static final String CREATE_DATE_TABLE_SQL = "CREATE TABLE" + " " +
64             DataProviderContract.DATE_TABLE_NAME + " " +
65             "(" + " " +
66             DataProviderContract.ROW_ID + " " + PRIMARY_KEY_TYPE + " ," +
67             DataProviderContract.DATA_DATE_COLUMN + " " + INTEGER_TYPE +
68             ")";
69 
70     // Identifies log statements issued by this component
71     public static final String LOG_TAG = "DataProvider";
72 
73     // Defines an helper object for the backing database
74     private SQLiteOpenHelper mHelper;
75 
76     // Defines a helper object that matches content URIs to table-specific parameters
77     private static final UriMatcher sUriMatcher;
78 
79     // Stores the MIME types served by this provider
80     private static final SparseArray<String> sMimeTypes;
81 
82     /*
83      * Initializes meta-data used by the content provider:
84      * - UriMatcher that maps content URIs to codes
85      * - MimeType array that returns the custom MIME type of a table
86      */
87     static {
88 
89         // Creates an object that associates content URIs with numeric codes
90         sUriMatcher = new UriMatcher(0);
91 
92         /*
93          * Sets up an array that maps content URIs to MIME types, via a mapping between the
94          * URIs and an integer code. These are custom MIME types that apply to tables and rows
95          * in this particular provider.
96          */
97         sMimeTypes = new SparseArray<String>();
98 
99         // Adds a URI "match" entry that maps picture URL content URIs to a numeric code
sUriMatcher.addURI( DataProviderContract.AUTHORITY, DataProviderContract.PICTUREURL_TABLE_NAME, IMAGE_URL_QUERY)100         sUriMatcher.addURI(
101                 DataProviderContract.AUTHORITY,
102                 DataProviderContract.PICTUREURL_TABLE_NAME,
103                 IMAGE_URL_QUERY);
104 
105         // Adds a URI "match" entry that maps modification date content URIs to a numeric code
sUriMatcher.addURI( DataProviderContract.AUTHORITY, DataProviderContract.DATE_TABLE_NAME, URL_DATE_QUERY)106         sUriMatcher.addURI(
107             DataProviderContract.AUTHORITY,
108             DataProviderContract.DATE_TABLE_NAME,
109             URL_DATE_QUERY);
110 
111         // Specifies a custom MIME type for the picture URL table
sMimeTypes.put( IMAGE_URL_QUERY, "vnd.android.cursor.dir/vnd." + DataProviderContract.AUTHORITY + "." + DataProviderContract.PICTUREURL_TABLE_NAME)112         sMimeTypes.put(
113                 IMAGE_URL_QUERY,
114                 "vnd.android.cursor.dir/vnd." +
115                 DataProviderContract.AUTHORITY + "." +
116                 DataProviderContract.PICTUREURL_TABLE_NAME);
117 
118         // Specifies the custom MIME type for a single modification date row
sMimeTypes.put( URL_DATE_QUERY, "vnd.android.cursor.item/vnd."+ DataProviderContract.AUTHORITY + "." + DataProviderContract.DATE_TABLE_NAME)119         sMimeTypes.put(
120                 URL_DATE_QUERY,
121                 "vnd.android.cursor.item/vnd."+
122                 DataProviderContract.AUTHORITY + "." +
123                 DataProviderContract.DATE_TABLE_NAME);
124     }
125 
126     // Closes the SQLite database helper class, to avoid memory leaks
close()127     public void close() {
128         mHelper.close();
129     }
130 
131     /**
132      * Defines a helper class that opens the SQLite database for this provider when a request is
133      * received. If the database doesn't yet exist, the helper creates it.
134      */
135     private class DataProviderHelper extends SQLiteOpenHelper {
136         /**
137          * Instantiates a new SQLite database using the supplied database name and version
138          *
139          * @param context The current context
140          */
DataProviderHelper(Context context)141         DataProviderHelper(Context context) {
142             super(context,
143                     DataProviderContract.DATABASE_NAME,
144                     null,
145                     DataProviderContract.DATABASE_VERSION);
146         }
147 
148 
149         /**
150          * Executes the queries to drop all of the tables from the database.
151          *
152          * @param db A handle to the provider's backing database.
153          */
dropTables(SQLiteDatabase db)154         private void dropTables(SQLiteDatabase db) {
155 
156             // If the table doesn't exist, don't throw an error
157             db.execSQL("DROP TABLE IF EXISTS " + DataProviderContract.PICTUREURL_TABLE_NAME);
158             db.execSQL("DROP TABLE IF EXISTS " + DataProviderContract.DATE_TABLE_NAME);
159         }
160 
161         /**
162          * Does setup of the database. The system automatically invokes this method when
163          * SQLiteDatabase.getWriteableDatabase() or SQLiteDatabase.getReadableDatabase() are
164          * invoked and no db instance is available.
165          *
166          * @param db the database instance in which to create the tables.
167          */
168         @Override
onCreate(SQLiteDatabase db)169         public void onCreate(SQLiteDatabase db) {
170             // Creates the tables in the backing database for this provider
171             db.execSQL(CREATE_PICTUREURL_TABLE_SQL);
172             db.execSQL(CREATE_DATE_TABLE_SQL);
173 
174         }
175 
176         /**
177          * Handles upgrading the database from a previous version. Drops the old tables and creates
178          * new ones.
179          *
180          * @param db The database to upgrade
181          * @param version1 The old database version
182          * @param version2 The new database version
183          */
184         @Override
onUpgrade(SQLiteDatabase db, int version1, int version2)185         public void onUpgrade(SQLiteDatabase db, int version1, int version2) {
186             Log.w(DataProviderHelper.class.getName(),
187                     "Upgrading database from version " + version1 + " to "
188                             + version2 + ", which will destroy all the existing data");
189 
190             // Drops all the existing tables in the database
191             dropTables(db);
192 
193             // Invokes the onCreate callback to build new tables
194             onCreate(db);
195         }
196         /**
197          * Handles downgrading the database from a new to a previous version. Drops the old tables
198          * and creates new ones.
199          * @param db The database object to downgrade
200          * @param version1 The old database version
201          * @param version2 The new database version
202          */
203         @Override
onDowngrade(SQLiteDatabase db, int version1, int version2)204         public void onDowngrade(SQLiteDatabase db, int version1, int version2) {
205             Log.w(DataProviderHelper.class.getName(),
206                 "Downgrading database from version " + version1 + " to "
207                         + version2 + ", which will destroy all the existing data");
208 
209             // Drops all the existing tables in the database
210             dropTables(db);
211 
212             // Invokes the onCreate callback to build new tables
213             onCreate(db);
214 
215         }
216     }
217     /**
218      * Initializes the content provider. Notice that this method simply creates a
219      * the SQLiteOpenHelper instance and returns. You should do most of the initialization of a
220      * content provider in its static initialization block or in SQLiteDatabase.onCreate().
221      */
222     @Override
onCreate()223     public boolean onCreate() {
224 
225         // Creates a new database helper object
226         mHelper = new DataProviderHelper(getContext());
227 
228         return true;
229     }
230     /**
231      * Returns the result of querying the chosen table.
232      * @see android.content.ContentProvider#query(Uri, String[], String, String[], String)
233      * @param uri The content URI of the table
234      * @param projection The names of the columns to return in the cursor
235      * @param selection The selection clause for the query
236      * @param selectionArgs An array of Strings containing search criteria
237      * @param sortOrder A clause defining the order in which the retrieved rows should be sorted
238      * @return The query results, as a {@link android.database.Cursor} of rows and columns
239      */
240     @Override
query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)241     public Cursor query(
242         Uri uri,
243         String[] projection,
244         String selection,
245         String[] selectionArgs,
246         String sortOrder) {
247 
248         SQLiteDatabase db = mHelper.getReadableDatabase();
249         // Decodes the content URI and maps it to a code
250         switch (sUriMatcher.match(uri)) {
251 
252             // If the query is for a picture URL
253             case IMAGE_URL_QUERY:
254                 // Does the query against a read-only version of the database
255                 Cursor returnCursor = db.query(
256                     DataProviderContract.PICTUREURL_TABLE_NAME,
257                     projection,
258                     null, null, null, null, null);
259 
260                 // Sets the ContentResolver to watch this content URI for data changes
261                 returnCursor.setNotificationUri(getContext().getContentResolver(), uri);
262                 return returnCursor;
263 
264             // If the query is for a modification date URL
265             case URL_DATE_QUERY:
266                 returnCursor = db.query(
267                     DataProviderContract.DATE_TABLE_NAME,
268                     projection,
269                     selection,
270                     selectionArgs,
271                     null,
272                     null,
273                     sortOrder);
274 
275                 // No notification Uri is set, because the data doesn't have to be watched.
276                 return returnCursor;
277 
278             case INVALID_URI:
279 
280                 throw new IllegalArgumentException("Query -- Invalid URI:" + uri);
281         }
282 
283         return null;
284     }
285 
286     /**
287      * Returns the mimeType associated with the Uri (query).
288      * @see android.content.ContentProvider#getType(Uri)
289      * @param uri the content URI to be checked
290      * @return the corresponding MIMEtype
291      */
292     @Override
getType(Uri uri)293     public String getType(Uri uri) {
294 
295         return sMimeTypes.get(sUriMatcher.match(uri));
296     }
297     /**
298      *
299      * Insert a single row into a table
300      * @see android.content.ContentProvider#insert(Uri, ContentValues)
301      * @param uri the content URI of the table
302      * @param values a {@link android.content.ContentValues} object containing the row to insert
303      * @return the content URI of the new row
304      */
305     @Override
insert(Uri uri, ContentValues values)306     public Uri insert(Uri uri, ContentValues values) {
307 
308         // Decode the URI to choose which action to take
309         switch (sUriMatcher.match(uri)) {
310 
311             // For the modification date table
312             case URL_DATE_QUERY:
313 
314                 // Creates a writeable database or gets one from cache
315                 SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
316 
317                 // Inserts the row into the table and returns the new row's _id value
318                 long id = localSQLiteDatabase.insert(
319                         DataProviderContract.DATE_TABLE_NAME,
320                         DataProviderContract.DATA_DATE_COLUMN,
321                         values
322                 );
323 
324                 // If the insert succeeded, notify a change and return the new row's content URI.
325                 if (-1 != id) {
326                     getContext().getContentResolver().notifyChange(uri, null);
327                     return Uri.withAppendedPath(uri, Long.toString(id));
328                 } else {
329 
330                     throw new SQLiteException("Insert error:" + uri);
331                 }
332             case IMAGE_URL_QUERY:
333 
334                 throw new IllegalArgumentException("Insert: Invalid URI" + uri);
335         }
336 
337         return null;
338     }
339     /**
340      * Implements bulk row insertion using
341      * {@link SQLiteDatabase#insert(String, String, ContentValues) SQLiteDatabase.insert()}
342      * and SQLite transactions. The method also notifies the current
343      * {@link android.content.ContentResolver} that the {@link android.content.ContentProvider} has
344      * been changed.
345      * @see android.content.ContentProvider#bulkInsert(Uri, ContentValues[])
346      * @param uri The content URI for the insertion
347      * @param insertValuesArray A {@link android.content.ContentValues} array containing the row to
348      * insert
349      * @return The number of rows inserted.
350      */
351     @Override
bulkInsert(Uri uri, ContentValues[] insertValuesArray)352     public int bulkInsert(Uri uri, ContentValues[] insertValuesArray) {
353 
354         // Decodes the content URI and choose which insert to use
355         switch (sUriMatcher.match(uri)) {
356 
357             // picture URLs table
358             case IMAGE_URL_QUERY:
359 
360                 // Gets a writeable database instance if one is not already cached
361                 SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
362 
363                 /*
364                  * Begins a transaction in "exclusive" mode. No other mutations can occur on the
365                  * db until this transaction finishes.
366                  */
367                 localSQLiteDatabase.beginTransaction();
368 
369                 // Deletes all the existing rows in the table
370                 localSQLiteDatabase.delete(DataProviderContract.PICTUREURL_TABLE_NAME, null, null);
371 
372                 // Gets the size of the bulk insert
373                 int numImages = insertValuesArray.length;
374 
375                 // Inserts each ContentValues entry in the array as a row in the database
376                 for (int i = 0; i < numImages; i++) {
377 
378                     localSQLiteDatabase.insert(DataProviderContract.PICTUREURL_TABLE_NAME,
379                             DataProviderContract.IMAGE_URL_COLUMN, insertValuesArray[i]);
380                 }
381 
382                 // Reports that the transaction was successful and should not be backed out.
383                 localSQLiteDatabase.setTransactionSuccessful();
384 
385                 // Ends the transaction and closes the current db instances
386                 localSQLiteDatabase.endTransaction();
387                 localSQLiteDatabase.close();
388 
389                 /*
390                  * Notifies the current ContentResolver that the data associated with "uri" has
391                  * changed.
392                  */
393 
394                 getContext().getContentResolver().notifyChange(uri, null);
395 
396                 // The semantics of bulkInsert is to return the number of rows inserted.
397                 return numImages;
398 
399             // modification date table
400             case URL_DATE_QUERY:
401 
402                 // Do inserts by calling SQLiteDatabase.insert on each row in insertValuesArray
403                 return super.bulkInsert(uri, insertValuesArray);
404 
405             case INVALID_URI:
406 
407                 // An invalid URI was passed. Throw an exception
408                 throw new IllegalArgumentException("Bulk insert -- Invalid URI:" + uri);
409 
410         }
411 
412         return -1;
413 
414     }
415     /**
416      * Returns an UnsupportedOperationException if delete is called
417      * @see android.content.ContentProvider#delete(Uri, String, String[])
418      * @param uri The content URI
419      * @param selection The SQL WHERE string. Use "?" to mark places that should be substituted by
420      * values in selectionArgs.
421      * @param selectionArgs An array of values that are mapped to each "?" in selection. If no "?"
422      * are used, set this to NULL.
423      *
424      * @return the number of rows deleted
425      */
426     @Override
delete(Uri uri, String selection, String[] selectionArgs)427     public int delete(Uri uri, String selection, String[] selectionArgs) {
428 
429         throw new UnsupportedOperationException("Delete -- unsupported operation " + uri);
430     }
431 
432     /**
433      * Updates one or more rows in a table.
434      * @see android.content.ContentProvider#update(Uri, ContentValues, String, String[])
435      * @param uri The content URI for the table
436      * @param values The values to use to update the row or rows. You only need to specify column
437      * names for the columns you want to change. To clear the contents of a column, specify the
438      * column name and NULL for its value.
439      * @param selection An SQL WHERE clause (without the WHERE keyword) specifying the rows to
440      * update. Use "?" to mark places that should be substituted by values in selectionArgs.
441      * @param selectionArgs An array of values that are mapped in order to each "?" in selection.
442      * If no "?" are used, set this to NULL.
443      *
444      * @return int The number of rows updated.
445      */
446     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)447     public int update(Uri uri, ContentValues values, String selection,
448             String[] selectionArgs) {
449 
450         // Decodes the content URI and choose which insert to use
451         switch (sUriMatcher.match(uri)) {
452 
453             // A picture URL content URI
454             case URL_DATE_QUERY:
455 
456                 // Creats a new writeable database or retrieves a cached one
457                 SQLiteDatabase localSQLiteDatabase = mHelper.getWritableDatabase();
458 
459                 // Updates the table
460                 int rows = localSQLiteDatabase.update(
461                         DataProviderContract.DATE_TABLE_NAME,
462                         values,
463                         selection,
464                         selectionArgs);
465 
466                 // If the update succeeded, notify a change and return the number of updated rows.
467                 if (0 != rows) {
468                     getContext().getContentResolver().notifyChange(uri, null);
469                     return rows;
470                 } else {
471 
472                     throw new SQLiteException("Update error:" + uri);
473                 }
474 
475             case IMAGE_URL_QUERY:
476 
477                 throw new IllegalArgumentException("Update: Invalid URI: " + uri);
478         }
479 
480         return -1;
481     }
482 }
483