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