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 */ 16 package com.android.photos.data; 17 18 import android.content.ContentResolver; 19 import android.content.ContentUris; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.content.UriMatcher; 23 import android.database.Cursor; 24 import android.database.DatabaseUtils; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.database.sqlite.SQLiteQueryBuilder; 28 import android.media.ExifInterface; 29 import android.net.Uri; 30 import android.os.CancellationSignal; 31 import android.provider.BaseColumns; 32 33 import com.android.gallery3d.common.ApiHelper; 34 35 import 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 */ 51 public 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 { notifyChange(Uri uri, boolean syncToNetwork)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 /** 89 * The image_type query parameter required for requesting a specific 90 * size of image. 91 */ 92 public static final String MEDIA_SIZE_QUERY_PARAMETER = "media_size"; 93 94 /** Internal database table used for basic photo information. */ 95 public static final String TABLE = "photos"; 96 /** Content URI for basic photo and video information. */ 97 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 98 99 /** Long foreign key to Accounts._ID */ 100 public static final String ACCOUNT_ID = "account_id"; 101 /** Column name for the width of the original image. Integer value. */ 102 public static final String WIDTH = "width"; 103 /** Column name for the height of the original image. Integer value. */ 104 public static final String HEIGHT = "height"; 105 /** 106 * Column name for the date that the original image was taken. Long 107 * value indicating the milliseconds since epoch in the GMT time zone. 108 */ 109 public static final String DATE_TAKEN = "date_taken"; 110 /** 111 * Column name indicating the long value of the album id that this image 112 * resides in. Will be NULL if it it has not been uploaded to the 113 * server. 114 */ 115 public static final String ALBUM_ID = "album_id"; 116 /** The column name for the mime-type String. */ 117 public static final String MIME_TYPE = "mime_type"; 118 /** The title of the photo. String value. */ 119 public static final String TITLE = "title"; 120 /** The date the photo entry was last updated. Long value. */ 121 public static final String DATE_MODIFIED = "date_modified"; 122 /** 123 * The rotation of the photo in degrees, if rotation has not already 124 * been applied. Integer value. 125 */ 126 public static final String ROTATION = "rotation"; 127 } 128 129 /** 130 * Contains columns and Uri for accessing album information. 131 */ 132 public static interface Albums extends BaseColumns { 133 /** Internal database table used album information. */ 134 public static final String TABLE = "albums"; 135 /** Content URI for album information. */ 136 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 137 138 /** Long foreign key to Accounts._ID */ 139 public static final String ACCOUNT_ID = "account_id"; 140 /** Parent directory or null if this is in the root. */ 141 public static final String PARENT_ID = "parent_id"; 142 /** The type of album. Non-null, if album is auto-generated. String value. */ 143 public static final String ALBUM_TYPE = "album_type"; 144 /** 145 * Column name for the visibility level of the album. Can be any of the 146 * VISIBILITY_* values. 147 */ 148 public static final String VISIBILITY = "visibility"; 149 /** The user-specified location associated with the album. String value. */ 150 public static final String LOCATION_STRING = "location_string"; 151 /** The title of the album. String value. */ 152 public static final String TITLE = "title"; 153 /** A short summary of the contents of the album. String value. */ 154 public static final String SUMMARY = "summary"; 155 /** The date the album was created. Long value */ 156 public static final String DATE_PUBLISHED = "date_published"; 157 /** The date the album entry was last updated. Long value. */ 158 public static final String DATE_MODIFIED = "date_modified"; 159 160 // Privacy values for Albums.VISIBILITY 161 public static final int VISIBILITY_PRIVATE = 1; 162 public static final int VISIBILITY_SHARED = 2; 163 public static final int VISIBILITY_PUBLIC = 3; 164 } 165 166 /** 167 * Contains columns and Uri for accessing photo and video metadata 168 */ 169 public static interface Metadata extends BaseColumns { 170 /** Internal database table used metadata information. */ 171 public static final String TABLE = "metadata"; 172 /** Content URI for photo and video metadata. */ 173 public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, TABLE); 174 /** Foreign key to photo_id. Long value. */ 175 public static final String PHOTO_ID = "photo_id"; 176 /** Metadata key. String value */ 177 public static final String KEY = "key"; 178 /** 179 * Metadata value. Type is based on key. 180 */ 181 public static final String VALUE = "value"; 182 183 /** A short summary of the photo. String value. */ 184 public static final String KEY_SUMMARY = "summary"; 185 /** The date the photo was added. Long value. */ 186 public static final String KEY_PUBLISHED = "date_published"; 187 /** The date the photo was last updated. Long value. */ 188 public static final String KEY_DATE_UPDATED = "date_updated"; 189 /** The size of the photo is bytes. Integer value. */ 190 public static final String KEY_SIZE_IN_BTYES = "size"; 191 /** The latitude associated with the photo. Double value. */ 192 public static final String KEY_LATITUDE = "latitude"; 193 /** The longitude associated with the photo. Double value. */ 194 public static final String KEY_LONGITUDE = "longitude"; 195 196 /** The make of the camera used. String value. */ 197 public static final String KEY_EXIF_MAKE = ExifInterface.TAG_MAKE; 198 /** The model of the camera used. String value. */ 199 public static final String KEY_EXIF_MODEL = ExifInterface.TAG_MODEL;; 200 /** The exposure time used. Float value. */ 201 public static final String KEY_EXIF_EXPOSURE = ExifInterface.TAG_EXPOSURE_TIME; 202 /** Whether the flash was used. Boolean value. */ 203 public static final String KEY_EXIF_FLASH = ExifInterface.TAG_FLASH; 204 /** The focal length used. Float value. */ 205 public static final String KEY_EXIF_FOCAL_LENGTH = ExifInterface.TAG_FOCAL_LENGTH; 206 /** The fstop value used. Float value. */ 207 public static final String KEY_EXIF_FSTOP = ExifInterface.TAG_APERTURE; 208 /** The ISO equivalent value used. Integer value. */ 209 public static final String KEY_EXIF_ISO = ExifInterface.TAG_ISO; 210 } 211 212 // SQL used within this class. 213 protected static final String WHERE_ID = BaseColumns._ID + " = ?"; 214 protected static final String WHERE_METADATA_ID = Metadata.PHOTO_ID + " = ? AND " 215 + Metadata.KEY + " = ?"; 216 217 protected static final String SELECT_ALBUM_ID = "SELECT " + Albums._ID + " FROM " 218 + Albums.TABLE; 219 protected static final String SELECT_PHOTO_ID = "SELECT " + Photos._ID + " FROM " 220 + Photos.TABLE; 221 protected static final String SELECT_PHOTO_COUNT = "SELECT COUNT(_id) FROM " + Photos.TABLE; 222 protected static final String DELETE_PHOTOS = "DELETE FROM " + Photos.TABLE; 223 protected static final String DELETE_METADATA = "DELETE FROM " + Metadata.TABLE; 224 protected static final String SELECT_METADATA_COUNT = "SELECT COUNT(_id) FROM " + Metadata.TABLE; 225 protected static final String WHERE = " WHERE "; 226 protected static final String IN = " IN "; 227 protected static final String NESTED_SELECT_START = "("; 228 protected static final String NESTED_SELECT_END = ")"; 229 protected static final String[] PROJECTION_COUNT = { 230 "COUNT(_id)" 231 }; 232 233 /** 234 * For selecting the mime-type for an image. 235 */ 236 private static final String[] PROJECTION_MIME_TYPE = { 237 Photos.MIME_TYPE, 238 }; 239 240 protected static final String[] BASE_COLUMNS_ID = { 241 BaseColumns._ID, 242 }; 243 244 protected ChangeNotification mNotifier = null; 245 protected static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 246 247 protected static final int MATCH_PHOTO = 1; 248 protected static final int MATCH_PHOTO_ID = 2; 249 protected static final int MATCH_ALBUM = 3; 250 protected static final int MATCH_ALBUM_ID = 4; 251 protected static final int MATCH_METADATA = 5; 252 protected static final int MATCH_METADATA_ID = 6; 253 protected static final int MATCH_ACCOUNT = 7; 254 protected static final int MATCH_ACCOUNT_ID = 8; 255 256 static { sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO)257 sUriMatcher.addURI(AUTHORITY, Photos.TABLE, MATCH_PHOTO); 258 // match against Photos._ID sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID)259 sUriMatcher.addURI(AUTHORITY, Photos.TABLE + "/#", MATCH_PHOTO_ID); sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM)260 sUriMatcher.addURI(AUTHORITY, Albums.TABLE, MATCH_ALBUM); 261 // match against Albums._ID sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID)262 sUriMatcher.addURI(AUTHORITY, Albums.TABLE + "/#", MATCH_ALBUM_ID); sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA)263 sUriMatcher.addURI(AUTHORITY, Metadata.TABLE, MATCH_METADATA); 264 // match against metadata/<Metadata._ID> sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID)265 sUriMatcher.addURI(AUTHORITY, Metadata.TABLE + "/#", MATCH_METADATA_ID); sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT)266 sUriMatcher.addURI(AUTHORITY, Accounts.TABLE, MATCH_ACCOUNT); 267 // match against Accounts._ID sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID)268 sUriMatcher.addURI(AUTHORITY, Accounts.TABLE + "/#", MATCH_ACCOUNT_ID); 269 } 270 271 @Override deleteInTransaction(Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)272 public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs, 273 boolean callerIsSyncAdapter) { 274 int match = matchUri(uri); 275 selection = addIdToSelection(match, selection); 276 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 277 return deleteCascade(uri, match, selection, selectionArgs); 278 } 279 280 @Override getType(Uri uri)281 public String getType(Uri uri) { 282 Cursor cursor = query(uri, PROJECTION_MIME_TYPE, null, null, null); 283 String mimeType = null; 284 if (cursor.moveToNext()) { 285 mimeType = cursor.getString(0); 286 } 287 cursor.close(); 288 return mimeType; 289 } 290 291 @Override insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter)292 public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) { 293 int match = matchUri(uri); 294 validateMatchTable(match); 295 String table = getTableFromMatch(match, uri); 296 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 297 Uri insertedUri = null; 298 long id = db.insert(table, null, values); 299 if (id != -1) { 300 // uri already matches the table. 301 insertedUri = ContentUris.withAppendedId(uri, id); 302 postNotifyUri(insertedUri); 303 } 304 return insertedUri; 305 } 306 307 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)308 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 309 String sortOrder) { 310 return query(uri, projection, selection, selectionArgs, sortOrder, null); 311 } 312 313 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)314 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 315 String sortOrder, CancellationSignal cancellationSignal) { 316 projection = replaceCount(projection); 317 int match = matchUri(uri); 318 selection = addIdToSelection(match, selection); 319 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 320 String table = getTableFromMatch(match, uri); 321 Cursor c = query(table, projection, selection, selectionArgs, sortOrder, cancellationSignal); 322 if (c != null) { 323 c.setNotificationUri(getContext().getContentResolver(), uri); 324 } 325 return c; 326 } 327 328 @Override updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs, boolean callerIsSyncAdapter)329 public int updateInTransaction(Uri uri, ContentValues values, String selection, 330 String[] selectionArgs, boolean callerIsSyncAdapter) { 331 int match = matchUri(uri); 332 int rowsUpdated = 0; 333 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 334 if (match == MATCH_METADATA) { 335 rowsUpdated = modifyMetadata(db, values); 336 } else { 337 selection = addIdToSelection(match, selection); 338 selectionArgs = addIdToSelectionArgs(match, uri, selectionArgs); 339 String table = getTableFromMatch(match, uri); 340 rowsUpdated = db.update(table, values, selection, selectionArgs); 341 } 342 postNotifyUri(uri); 343 return rowsUpdated; 344 } 345 setMockNotification(ChangeNotification notification)346 public void setMockNotification(ChangeNotification notification) { 347 mNotifier = notification; 348 } 349 addIdToSelection(int match, String selection)350 protected static String addIdToSelection(int match, String selection) { 351 String where; 352 switch (match) { 353 case MATCH_PHOTO_ID: 354 case MATCH_ALBUM_ID: 355 case MATCH_METADATA_ID: 356 where = WHERE_ID; 357 break; 358 default: 359 return selection; 360 } 361 return DatabaseUtils.concatenateWhere(selection, where); 362 } 363 addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs)364 protected static String[] addIdToSelectionArgs(int match, Uri uri, String[] selectionArgs) { 365 String[] whereArgs; 366 switch (match) { 367 case MATCH_PHOTO_ID: 368 case MATCH_ALBUM_ID: 369 case MATCH_METADATA_ID: 370 whereArgs = new String[] { 371 uri.getPathSegments().get(1), 372 }; 373 break; 374 default: 375 return selectionArgs; 376 } 377 return DatabaseUtils.appendSelectionArgs(selectionArgs, whereArgs); 378 } 379 addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri)380 protected static String[] addMetadataKeysToSelectionArgs(String[] selectionArgs, Uri uri) { 381 List<String> segments = uri.getPathSegments(); 382 String[] additionalArgs = { 383 segments.get(1), 384 segments.get(2), 385 }; 386 387 return DatabaseUtils.appendSelectionArgs(selectionArgs, additionalArgs); 388 } 389 getTableFromMatch(int match, Uri uri)390 protected static String getTableFromMatch(int match, Uri uri) { 391 String table; 392 switch (match) { 393 case MATCH_PHOTO: 394 case MATCH_PHOTO_ID: 395 table = Photos.TABLE; 396 break; 397 case MATCH_ALBUM: 398 case MATCH_ALBUM_ID: 399 table = Albums.TABLE; 400 break; 401 case MATCH_METADATA: 402 case MATCH_METADATA_ID: 403 table = Metadata.TABLE; 404 break; 405 case MATCH_ACCOUNT: 406 case MATCH_ACCOUNT_ID: 407 table = Accounts.TABLE; 408 break; 409 default: 410 throw unknownUri(uri); 411 } 412 return table; 413 } 414 415 @Override getDatabaseHelper(Context context)416 public SQLiteOpenHelper getDatabaseHelper(Context context) { 417 return new PhotoDatabase(context, DB_NAME); 418 } 419 modifyMetadata(SQLiteDatabase db, ContentValues values)420 private int modifyMetadata(SQLiteDatabase db, ContentValues values) { 421 int rowCount; 422 if (values.get(Metadata.VALUE) == null) { 423 String[] selectionArgs = { 424 values.getAsString(Metadata.PHOTO_ID), values.getAsString(Metadata.KEY), 425 }; 426 rowCount = db.delete(Metadata.TABLE, WHERE_METADATA_ID, selectionArgs); 427 } else { 428 long rowId = db.replace(Metadata.TABLE, null, values); 429 rowCount = (rowId == -1) ? 0 : 1; 430 } 431 return rowCount; 432 } 433 matchUri(Uri uri)434 private int matchUri(Uri uri) { 435 int match = sUriMatcher.match(uri); 436 if (match == UriMatcher.NO_MATCH) { 437 throw unknownUri(uri); 438 } 439 return match; 440 } 441 442 @Override notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork)443 protected void notifyChange(ContentResolver resolver, Uri uri, boolean syncToNetwork) { 444 if (mNotifier != null) { 445 mNotifier.notifyChange(uri, syncToNetwork); 446 } else { 447 super.notifyChange(resolver, uri, syncToNetwork); 448 } 449 } 450 unknownUri(Uri uri)451 protected static IllegalArgumentException unknownUri(Uri uri) { 452 return new IllegalArgumentException("Unknown Uri format: " + uri); 453 } 454 nestWhere(String matchColumn, String table, String nestedWhere)455 protected static String nestWhere(String matchColumn, String table, String nestedWhere) { 456 String query = SQLiteQueryBuilder.buildQueryString(false, table, BASE_COLUMNS_ID, 457 nestedWhere, null, null, null, null); 458 return matchColumn + IN + NESTED_SELECT_START + query + NESTED_SELECT_END; 459 } 460 metadataSelectionFromPhotos(String where)461 protected static String metadataSelectionFromPhotos(String where) { 462 return nestWhere(Metadata.PHOTO_ID, Photos.TABLE, where); 463 } 464 photoSelectionFromAlbums(String where)465 protected static String photoSelectionFromAlbums(String where) { 466 return nestWhere(Photos.ALBUM_ID, Albums.TABLE, where); 467 } 468 photoSelectionFromAccounts(String where)469 protected static String photoSelectionFromAccounts(String where) { 470 return nestWhere(Photos.ACCOUNT_ID, Accounts.TABLE, where); 471 } 472 albumSelectionFromAccounts(String where)473 protected static String albumSelectionFromAccounts(String where) { 474 return nestWhere(Albums.ACCOUNT_ID, Accounts.TABLE, where); 475 } 476 deleteCascade(Uri uri, int match, String selection, String[] selectionArgs)477 protected int deleteCascade(Uri uri, int match, String selection, String[] selectionArgs) { 478 switch (match) { 479 case MATCH_PHOTO: 480 case MATCH_PHOTO_ID: 481 deleteCascade(Metadata.CONTENT_URI, MATCH_METADATA, 482 metadataSelectionFromPhotos(selection), selectionArgs); 483 break; 484 case MATCH_ALBUM: 485 case MATCH_ALBUM_ID: 486 deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO, 487 photoSelectionFromAlbums(selection), selectionArgs); 488 break; 489 case MATCH_ACCOUNT: 490 case MATCH_ACCOUNT_ID: 491 deleteCascade(Photos.CONTENT_URI, MATCH_PHOTO, 492 photoSelectionFromAccounts(selection), selectionArgs); 493 deleteCascade(Albums.CONTENT_URI, MATCH_ALBUM, 494 albumSelectionFromAccounts(selection), selectionArgs); 495 break; 496 } 497 SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); 498 String table = getTableFromMatch(match, uri); 499 int deleted = db.delete(table, selection, selectionArgs); 500 if (deleted > 0) { 501 postNotifyUri(uri); 502 } 503 return deleted; 504 } 505 validateMatchTable(int match)506 private static void validateMatchTable(int match) { 507 switch (match) { 508 case MATCH_PHOTO: 509 case MATCH_ALBUM: 510 case MATCH_METADATA: 511 case MATCH_ACCOUNT: 512 break; 513 default: 514 throw new IllegalArgumentException("Operation not allowed on an existing row."); 515 } 516 } 517 query(String table, String[] columns, String selection, String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal)518 protected Cursor query(String table, String[] columns, String selection, 519 String[] selectionArgs, String orderBy, CancellationSignal cancellationSignal) { 520 SQLiteDatabase db = getDatabaseHelper().getReadableDatabase(); 521 if (ApiHelper.HAS_CANCELLATION_SIGNAL) { 522 return db.query(false, table, columns, selection, selectionArgs, null, null, 523 orderBy, null, cancellationSignal); 524 } else { 525 return db.query(table, columns, selection, selectionArgs, null, null, orderBy); 526 } 527 } 528 replaceCount(String[] projection)529 protected static String[] replaceCount(String[] projection) { 530 if (projection != null && projection.length == 1 531 && BaseColumns._COUNT.equals(projection[0])) { 532 return PROJECTION_COUNT; 533 } 534 return projection; 535 } 536 } 537