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