PhotoProvider.java revision 693a5b7a18066d4757d49a44214356a3dd9f61a0
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 */ 16package com.android.photos.data; 17 18import android.content.ContentResolver; 19import android.content.ContentUris; 20import android.content.ContentValues; 21import android.content.Context; 22import android.content.UriMatcher; 23import android.database.Cursor; 24import android.database.DatabaseUtils; 25import android.database.sqlite.SQLiteDatabase; 26import android.database.sqlite.SQLiteOpenHelper; 27import android.database.sqlite.SQLiteQueryBuilder; 28import android.media.ExifInterface; 29import android.net.Uri; 30import android.os.CancellationSignal; 31import android.provider.BaseColumns; 32 33import java.util.List; 34 35/** 36 * A provider that gives access to photo and video information for media stored 37 * on the server. Only media that is or will be put on the server will be 38 * accessed by this provider. Use Photos.CONTENT_URI to query all photos and 39 * videos. Use Albums.CONTENT_URI to query all albums. Use Metadata.CONTENT_URI 40 * to query metadata about a photo or video, based on the ID of the media. Use 41 * ImageCache.THUMBNAIL_CONTENT_URI, ImageCache.PREVIEW_CONTENT_URI, or 42 * ImageCache.ORIGINAL_CONTENT_URI to query the path of the thumbnail, preview, 43 * or original-sized image respectfully. <br/> 44 * To add or update metadata, use the update function rather than insert. All 45 * values for the metadata must be in the ContentValues, even if they are also 46 * in the selection. The selection and selectionArgs are not used when updating 47 * metadata. If the metadata values are null, the row will be deleted. 48 */ 49public class PhotoProvider extends SQLiteContentProvider { 50 @SuppressWarnings("unused") 51 private static final String TAG = PhotoProvider.class.getSimpleName(); 52 53 protected static final String DB_NAME = "photo.db"; 54 public static final String AUTHORITY = PhotoProviderAuthority.AUTHORITY; 55 static final Uri BASE_CONTENT_URI = new Uri.Builder().scheme("content").authority(AUTHORITY) 56 .build(); 57 58 // Used to allow mocking out the change notification because 59 // MockContextResolver disallows system-wide notification. 60 public static interface ChangeNotification { 61 void notifyChange(Uri uri, boolean syncToNetwork); 62 } 63 64 /** 65 * Contains columns that can be accessed via Accounts.CONTENT_URI 66 */ 67 public static interface Accounts extends BaseColumns { 68 /** 69 * Internal database table used for account information 70 */ 71 public static final String TABLE = "accounts"; 72 /** 73 * Content URI for account information 74 */ 75 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 76 /** 77 * User name for this account. 78 */ 79 public static final String ACCOUNT_NAME = "name"; 80 } 81 82 /** 83 * Contains columns that can be accessed via Photos.CONTENT_URI. 84 */ 85 public static interface Photos extends BaseColumns { 86 /** Internal database table used for basic photo information. */ 87 public static final String TABLE = "photos"; 88 /** Content URI for basic photo and video information. */ 89 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 90 91 /** Long foreign key to Accounts._ID */ 92 public static final String ACCOUNT_ID = "account_id"; 93 /** Column name for the width of the original image. Integer value. */ 94 public static final String WIDTH = "width"; 95 /** Column name for the height of the original image. Integer value. */ 96 public static final String HEIGHT = "height"; 97 /** 98 * Column name for the date that the original image was taken. Long 99 * value indicating the milliseconds since epoch in the GMT time zone. 100 */ 101 public static final String DATE_TAKEN = "date_taken"; 102 /** 103 * Column name indicating the long value of the album id that this image 104 * resides in. Will be NULL if it it has not been uploaded to the 105 * server. 106 */ 107 public static final String ALBUM_ID = "album_id"; 108 /** The column name for the mime-type String. */ 109 public static final String MIME_TYPE = "mime_type"; 110 /** The title of the photo. String value. */ 111 public static final String TITLE = "title"; 112 /** The date the photo entry was last updated. Long value. */ 113 public static final String DATE_MODIFIED = "date_modified"; 114 /** 115 * The rotation of the photo in degrees, if rotation has not already 116 * been applied. Integer value. 117 */ 118 public static final String ROTATION = "rotation"; 119 } 120 121 /** 122 * Contains columns and Uri for accessing album information. 123 */ 124 public static interface Albums extends BaseColumns { 125 /** Internal database table used album information. */ 126 public static final String TABLE = "albums"; 127 /** Content URI for album information. */ 128 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 129 130 /** Long foreign key to Accounts._ID */ 131 public static final String ACCOUNT_ID = "account_id"; 132 /** Parent directory or null if this is in the root. */ 133 public static final String PARENT_ID = "parent_id"; 134 /** 135 * Column name for the visibility level of the album. Can be any of the 136 * VISIBILITY_* values. 137 */ 138 public static final String VISIBILITY = "visibility"; 139 /** The user-specified location associated with the album. String value. */ 140 public static final String LOCATION_STRING = "location_string"; 141 /** The title of the album. String value. */ 142 public static final String TITLE = "title"; 143 /** A short summary of the contents of the album. String value. */ 144 public static final String SUMMARY = "summary"; 145 /** The date the album was created. Long value */ 146 public static final String DATE_PUBLISHED = "date_published"; 147 /** The date the album entry was last updated. Long value. */ 148 public static final String DATE_MODIFIED = "date_modified"; 149 150 // Privacy values for Albums.VISIBILITY 151 public static final int VISIBILITY_PRIVATE = 1; 152 public static final int VISIBILITY_SHARED = 2; 153 public static final int VISIBILITY_PUBLIC = 3; 154 } 155 156 /** 157 * Contains columns and Uri for accessing photo and video metadata 158 */ 159 public static interface Metadata extends BaseColumns { 160 /** Internal database table used metadata information. */ 161 public static final String TABLE = "metadata"; 162 /** Content URI for photo and video metadata. */ 163 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 164 /** Foreign key to photo_id. Long value. */ 165 public static final String PHOTO_ID = "photo_id"; 166 /** Metadata key. String value */ 167 public static final String KEY = "key"; 168 /** 169 * Metadata value. Type is based on key. 170 */ 171 public static final String VALUE = "value"; 172 173 /** A short summary of the photo. String value. */ 174 public static final String KEY_SUMMARY = "summary"; 175 /** The date the photo was added. Long value. */ 176 public static final String KEY_PUBLISHED = "date_published"; 177 /** The date the photo was last updated. Long value. */ 178 public static final String KEY_DATE_UPDATED = "date_updated"; 179 /** The size of the photo is bytes. Integer value. */ 180 public static final String KEY_SIZE_IN_BTYES = "size"; 181 /** The latitude associated with the photo. Double value. */ 182 public static final String KEY_LATITUDE = "latitude"; 183 /** The longitude associated with the photo. Double value. */ 184 public static final String KEY_LONGITUDE = "longitude"; 185 186 /** The make of the camera used. String value. */ 187 public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE; 188 /** The model of the camera used. String value. */ 189 public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;; 190 /** The exposure time used. Float value. */ 191 public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME; 192 /** Whether the flash was used. Boolean value. */ 193 public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH; 194 /** The focal length used. Float value. */ 195 public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH; 196 /** The fstop value used. Float value. */ 197 public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE; 198 /** The ISO equivalent value used. Integer value. */ 199 public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO; 200 } 201 202 /** 203 * Contains columns and Uri for maintaining the image cache. 204 */ 205 public static interface ImageCache extends BaseColumns { 206 /** Internal database table used for the image cache */ 207 public static final String TABLE = "image_cache"; 208 209 /** 210 * The image_type query parameter required for accessing a specific 211 * image 212 */ 213 public static final String IMAGE_TYPE_QUERY_PARAMETER = "image_type"; 214 215 // ImageCache.IMAGE_TYPE values 216 public static final int IMAGE_TYPE_ALBUM_COVER = 1; 217 public static final int IMAGE_TYPE_THUMBNAIL = 2; 218 public static final int IMAGE_TYPE_PREVIEW = 3; 219 public static final int IMAGE_TYPE_ORIGINAL = 4; 220 221 /** 222 * Content URI for retrieving image paths. The 223 * IMAGE_TYPE_QUERY_PARAMETER must be used in queries. 224 */ 225 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 226 227 /** 228 * Content URI for retrieving the album cover art. The album ID must be 229 * appended to the URI. 230 */ 231 public static final Uri ALBUM_COVER_CONTENT_URI = Uri.withAppendedPath(CONTENT_URI, 232 Albums.TABLE); 233 234 /** 235 * An _ID from Albums or Photos, depending on whether IMAGE_TYPE is 236 * IMAGE_TYPE_ALBUM or not. Long value. 237 */ 238 public static final String REMOTE_ID = "remote_id"; 239 /** One of IMAGE_TYPE_* values. */ 240 public static final String IMAGE_TYPE = "image_type"; 241 /** The String path to the image. */ 242 public static final String PATH = "path"; 243 }; 244 245 // SQL used within this class. 246 protected static final String WHERE_ID = BaseColumns._ID + " = ?"; 247 protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND " 248 + Metadata.KEY + " = ?"; 249 250 protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM " 251 + Albums.TABLE; 252 protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM " 253 + Photos.TABLE; 254 protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(*) FROM " + Photos.TABLE; 255 protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE; 256 protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE; 257 protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(*) FROM " + Metadata.TABLE; 258 protected static final String WHERE = " WHERE "; 259 protected static final String IN = " IN "; 260 protected static final String NESTED_SELECT_START = "("; 261 protected static final String NESTED_SELECT_END = ")"; 262 263 /** 264 * For selecting the mime-type for an image. 265 */ 266 private static final String[] PROJECTION_MIME_TYPE = { 267 Photos.MIME_TYPE, 268 }; 269 270 protected static final String[] BASE_COLUMNS_ID = { 271 BaseColumns._ID, 272 }; 273 274 protected ChangeNotification mNotifier = null; 275 protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 276 277 protected static final int MATCH_PHOTO = 1; 278 protected static final int MATCH_PHOTO_ID = 2; 279 protected static final int MATCH_ALBUM = 3; 280 protected static final int MATCH_ALBUM_ID = 4; 281 protected static final int MATCH_METADATA = 5; 282 protected static final int MATCH_METADATA_ID = 6; 283 protected static final int MATCH_IMAGE = 7; 284 protected static final int MATCH_ALBUM_COVER = 8; 285 protected static final int MATCH_ACCOUNT = 9; 286 protected static final int MATCH_ACCOUNT_ID = 10; 287 288 static { 289 sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO); 290 // match against Photos._ID 291 sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID); 292 sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM); 293 // match against Albums._ID 294 sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID); 295 sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA); 296 // match against metadata/<Metadata._ID> 297 sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID); 298 // match against image_cache/<ImageCache.PHOTO_ID> 299 sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/#", MATCH_IMAGE); 300 // match against image_cache/album/<Albums._ID> 301 sUriMatcher.addURI(AUTHORITY, ImageCache.TABLE + "/" + Albums.TABLE + "/#", 302 MATCH_ALBUM_COVER); 303 sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT); 304 // match against Accounts._ID 305 sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID); 306 } 307 308 @Override 309 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, 310 boolean callerIsSyncAdapter) { 311 int match = matchUri(uri); 312 selection = addIdToSelection(match, selection); 313 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 314 int deleted = 0; 315 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 316 deleted = deleteCascade(db, match, selection, selectionArgs, uri); 317 return deleted; 318 } 319 320 @Override 321 public String getType(Uri uri) { 322 Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null); 323 String mimeType = null; 324 if (cursor.moveToNext()) { 325 mimeType = cursor.getString(0); 326 } 327 cursor.close(); 328 return mimeType; 329 } 330 331 @Override 332 public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 333 int match = matchUri(uri); 334 validateMatchTable(match); 335 String table = getTableFromMatch(match, uri); 336 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 337 Uri insertedUri = null; 338 long id = db.insert(table, null, values); 339 if (id != -1) { 340 // uri already matches the table. 341 insertedUri = ContentUris.withAppendedId(uri, id); 342 postNotifyUri(insertedUri); 343 } 344 return insertedUri; 345 } 346 347 @Override 348 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 349 String sortOrder) { 350 return query(uri, projection, selection, selectionArgs, sortOrder, null); 351 } 352 353 @Override 354 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 355 String sortOrder, CancellationSignal cancellationSignal) { 356 int match = matchUri(uri); 357 selection = addIdToSelection(match, selection); 358 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 359 String table = getTableFromMatch(match, uri); 360 SQLiteDatabase db = getDatabaseHelper().getReadableDatabase(); 361 return db.query(false, table, projection, selection, selectionArgs, null, null, sortOrder, 362 null, cancellationSignal); 363 } 364 365 @Override 366 public int updateInTransaction(Uri uri, ContentValues values, String selection, 367 String[] selectionArgs, boolean callerIsSyncAdapter) { 368 int match = matchUri(uri); 369 int rowsUpdated = 0; 370 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 371 if (match == MATCH_METADATA) { 372 rowsUpdated = modifyMetadata(db, values); 373 } else { 374 selection = addIdToSelection(match, selection); 375 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 376 String table = getTableFromMatch(match, uri); 377 rowsUpdated = db.update(table, values, selection, selectionArgs); 378 } 379 postNotifyUri(uri); 380 return rowsUpdated; 381 } 382 383 public void setMockNotification(ChangeNotification notification) { 384 mNotifier = notification; 385 } 386 387 protected static String addIdToSelection(int match, String selection) { 388 String where; 389 switch (match) { 390 case MATCH_PHOTO_ID: 391 case MATCH_ALBUM_ID: 392 case MATCH_METADATA_ID: 393 where = WHERE_ID; 394 break; 395 default: 396 return selection; 397 } 398 return DatabaseUtils.concatenateWhere(selection, where); 399 } 400 401 protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) { 402 String[] whereArgs; 403 switch (match) { 404 case MATCH_PHOTO_ID: 405 case MATCH_ALBUM_ID: 406 case MATCH_METADATA_ID: 407 whereArgs = new String[] { 408 uri.getPathSegments().get(1), 409 }; 410 break; 411 default: 412 return selectionArgs; 413 } 414 return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs); 415 } 416 417 protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) { 418 List<String> segments = uri.getPathSegments(); 419 String[] additionalArgs = { 420 segments.get(1), 421 segments.get(2), 422 }; 423 424 return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs); 425 } 426 427 protected static String getTableFromMatch(int match, Uri uri) { 428 String table; 429 switch (match) { 430 case MATCH_PHOTO: 431 case MATCH_PHOTO_ID: 432 table = Photos.TABLE; 433 break; 434 case MATCH_ALBUM: 435 case MATCH_ALBUM_ID: 436 table = Albums.TABLE; 437 break; 438 case MATCH_METADATA: 439 case MATCH_METADATA_ID: 440 table = Metadata.TABLE; 441 break; 442 case MATCH_ACCOUNT: 443 case MATCH_ACCOUNT_ID: 444 table = Accounts.TABLE; 445 break; 446 default: 447 throw unknownUri(uri); 448 } 449 return table; 450 } 451 452 @Override 453 public SQLiteOpenHelper getDatabaseHelper(Context context) { 454 return new PhotoDatabase(context, DB_NAME); 455 } 456 457 private int modifyMetadata(SQLiteDatabase db, ContentValues values) { 458 int rowCount; 459 if (values.get(Metadata.VALUE) == null) { 460 String[] selectionArgs = { 461 values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY), 462 }; 463 rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs); 464 } else { 465 long rowId = db.replace(Metadata.TABLE, null, values); 466 rowCount = (rowId == -1) ? 0 : 1; 467 } 468 return rowCount; 469 } 470 471 private int matchUri(Uri uri) { 472 int match = sUriMatcher.match(uri); 473 if (match == UriMatcher.NO_MATCH) { 474 throw unknownUri(uri); 475 } 476 if (match == MATCH_IMAGE || match == MATCH_ALBUM_COVER) { 477 throw new IllegalArgumentException("Operation not allowed on image cache database"); 478 } 479 return match; 480 } 481 482 @Override 483 protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { 484 if (mNotifier != null) { 485 mNotifier.notifyChange(uri, syncToNetwork); 486 } else { 487 resolver.notifyChange(uri, null, syncToNetwork); 488 } 489 } 490 491 protected static IllegalArgumentException unknownUri(Uri uri) { 492 return new IllegalArgumentException("Unknown Uri format: " + uri); 493 } 494 495 protected static String nestWhere(String matchColumn, String table, String nestedWhere) { 496 String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID, 497 nestedWhere, null, null, null, null); 498 return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END; 499 } 500 501 protected int deleteCascade(SQLiteDatabase db, int match, String selection, 502 String[] selectionArgs, Uri uri) { 503 switch (match) { 504 case MATCH_PHOTO: 505 case MATCH_PHOTO_ID: 506 deleteCascadeMetadata(db, selection, selectionArgs); 507 break; 508 case MATCH_ALBUM: 509 case MATCH_ALBUM_ID: 510 deleteCascadePhotos(db, selection, selectionArgs); 511 break; 512 case MATCH_ACCOUNT: 513 case MATCH_ACCOUNT_ID: 514 deleteCascadeAccounts(db, selection, selectionArgs); 515 break; 516 } 517 String table = getTableFromMatch(match, uri); 518 int deleted = db.delete(table, selection, selectionArgs); 519 if (deleted > 0) { 520 postNotifyUri(uri); 521 } 522 return deleted; 523 } 524 525 private void deleteCascadeAccounts(SQLiteDatabase db, String accountSelect, String[] args) { 526 // Delete all photos associated with the account 527 String photoWhere = nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, accountSelect); 528 deleteCascadeMetadata(db, photoWhere, args); 529 db.delete(Photos.TABLE, photoWhere, args); 530 531 // Delete all albums that are associated with this account 532 String albumWhere = nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, accountSelect); 533 db.delete(Albums.TABLE, albumWhere, args); 534 } 535 536 private void deleteCascadePhotos(SQLiteDatabase db, String albumSelect, 537 String[] selectArgs) { 538 String photoWhere = nestWhere(Photos.ALBUM_ID, Albums.TABLE, albumSelect); 539 deleteCascadeMetadata(db, photoWhere, selectArgs); 540 int deleted = db.delete(Photos.TABLE, photoWhere, selectArgs); 541 if (deleted > 0) { 542 postNotifyUri(Photos.CONTENT_URI); 543 } 544 } 545 546 private void deleteCascadeMetadata(SQLiteDatabase db, String photosSelect, 547 String[] selectArgs) { 548 String metadataWhere = nestWhere(Metadata.PHOTO_ID, Photos.TABLE, photosSelect); 549 int deleted = db.delete(Metadata.TABLE, metadataWhere, selectArgs); 550 if (deleted > 0) { 551 postNotifyUri(Metadata.CONTENT_URI); 552 } 553 } 554 555 private static void validateMatchTable(int match) { 556 switch (match) { 557 case MATCH_PHOTO: 558 case MATCH_ALBUM: 559 case MATCH_METADATA: 560 case MATCH_ACCOUNT: 561 break; 562 default: 563 throw new IllegalArgumentException("Operation not allowed on an existing row."); 564 } 565 } 566} 567