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