/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.gallery3d.data; import android.annotation.TargetApi; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.drm.DrmHelper; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapRegionDecoder; import android.net.Uri; import android.os.Build; import android.provider.MediaStore.Images; import android.provider.MediaStore.Images.ImageColumns; import android.provider.MediaStore.MediaColumns; import android.util.Log; import com.android.gallery3d.app.GalleryApp; import com.android.gallery3d.app.PanoramaMetadataSupport; import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.common.BitmapUtils; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.filtershow.tools.SaveImage; import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.ThreadPool.Job; import com.android.gallery3d.util.ThreadPool.JobContext; import com.android.gallery3d.util.UpdateHelper; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; // LocalImage represents an image in the local storage. public class LocalImage extends LocalMediaItem { private static final String TAG = "LocalImage"; static final Path ITEM_PATH = Path.fromString("/local/image/item"); // Must preserve order between these indices and the order of the terms in // the following PROJECTION array. private static final int INDEX_ID = 0; private static final int INDEX_CAPTION = 1; private static final int INDEX_MIME_TYPE = 2; private static final int INDEX_LATITUDE = 3; private static final int INDEX_LONGITUDE = 4; private static final int INDEX_DATE_TAKEN = 5; private static final int INDEX_DATE_ADDED = 6; private static final int INDEX_DATE_MODIFIED = 7; private static final int INDEX_DATA = 8; private static final int INDEX_ORIENTATION = 9; private static final int INDEX_BUCKET_ID = 10; private static final int INDEX_SIZE = 11; private static final int INDEX_WIDTH = 12; private static final int INDEX_HEIGHT = 13; static final String[] PROJECTION = { ImageColumns._ID, // 0 ImageColumns.TITLE, // 1 ImageColumns.MIME_TYPE, // 2 ImageColumns.LATITUDE, // 3 ImageColumns.LONGITUDE, // 4 ImageColumns.DATE_TAKEN, // 5 ImageColumns.DATE_ADDED, // 6 ImageColumns.DATE_MODIFIED, // 7 ImageColumns.DATA, // 8 ImageColumns.ORIENTATION, // 9 ImageColumns.BUCKET_ID, // 10 ImageColumns.SIZE, // 11 "0", // 12 "0" // 13 }; static { updateWidthAndHeightProjection(); } @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private static void updateWidthAndHeightProjection() { if (ApiHelper.HAS_MEDIA_COLUMNS_WIDTH_AND_HEIGHT) { PROJECTION[INDEX_WIDTH] = MediaColumns.WIDTH; PROJECTION[INDEX_HEIGHT] = MediaColumns.HEIGHT; } } private final GalleryApp mApplication; public int rotation; private PanoramaMetadataSupport mPanoramaMetadata = new PanoramaMetadataSupport(this); public LocalImage(Path path, GalleryApp application, Cursor cursor) { super(path, nextVersionNumber()); mApplication = application; loadFromCursor(cursor); } public LocalImage(Path path, GalleryApp application, int id) { super(path, nextVersionNumber()); mApplication = application; ContentResolver resolver = mApplication.getContentResolver(); Uri uri = Images.Media.EXTERNAL_CONTENT_URI; Cursor cursor = LocalAlbum.getItemCursor(resolver, uri, PROJECTION, id); if (cursor == null) { throw new RuntimeException("cannot get cursor for: " + path); } try { if (cursor.moveToNext()) { loadFromCursor(cursor); } else { throw new RuntimeException("cannot find data for: " + path); } } finally { cursor.close(); } } private void loadFromCursor(Cursor cursor) { id = cursor.getInt(INDEX_ID); caption = cursor.getString(INDEX_CAPTION); mimeType = cursor.getString(INDEX_MIME_TYPE); latitude = cursor.getDouble(INDEX_LATITUDE); longitude = cursor.getDouble(INDEX_LONGITUDE); dateTakenInMs = cursor.getLong(INDEX_DATE_TAKEN); dateAddedInSec = cursor.getLong(INDEX_DATE_ADDED); dateModifiedInSec = cursor.getLong(INDEX_DATE_MODIFIED); filePath = cursor.getString(INDEX_DATA); rotation = cursor.getInt(INDEX_ORIENTATION); bucketId = cursor.getInt(INDEX_BUCKET_ID); fileSize = cursor.getLong(INDEX_SIZE); width = cursor.getInt(INDEX_WIDTH); height = cursor.getInt(INDEX_HEIGHT); } @Override protected boolean updateFromCursor(Cursor cursor) { UpdateHelper uh = new UpdateHelper(); id = uh.update(id, cursor.getInt(INDEX_ID)); caption = uh.update(caption, cursor.getString(INDEX_CAPTION)); mimeType = uh.update(mimeType, cursor.getString(INDEX_MIME_TYPE)); latitude = uh.update(latitude, cursor.getDouble(INDEX_LATITUDE)); longitude = uh.update(longitude, cursor.getDouble(INDEX_LONGITUDE)); dateTakenInMs = uh.update( dateTakenInMs, cursor.getLong(INDEX_DATE_TAKEN)); dateAddedInSec = uh.update( dateAddedInSec, cursor.getLong(INDEX_DATE_ADDED)); dateModifiedInSec = uh.update( dateModifiedInSec, cursor.getLong(INDEX_DATE_MODIFIED)); filePath = uh.update(filePath, cursor.getString(INDEX_DATA)); rotation = uh.update(rotation, cursor.getInt(INDEX_ORIENTATION)); bucketId = uh.update(bucketId, cursor.getInt(INDEX_BUCKET_ID)); fileSize = uh.update(fileSize, cursor.getLong(INDEX_SIZE)); width = uh.update(width, cursor.getInt(INDEX_WIDTH)); height = uh.update(height, cursor.getInt(INDEX_HEIGHT)); return uh.isUpdated(); } @Override public Job requestImage(int type) { return new LocalImageRequest(mApplication, mPath, dateModifiedInSec, type, filePath, mimeType); } public static class LocalImageRequest extends ImageCacheRequest { private String mLocalFilePath; LocalImageRequest(GalleryApp application, Path path, long timeModified, int type, String localFilePath) { super(application, path, timeModified, type, MediaItem.getTargetSize(type)); mLocalFilePath = localFilePath; } LocalImageRequest(GalleryApp application, Path path, long timeModified, int type, String localFilePath, String mimeType) { super(application, path, timeModified, type, MediaItem.getTargetSize(type),localFilePath, mimeType); mLocalFilePath = localFilePath; } @Override public Bitmap onDecodeOriginal(JobContext jc, final int type) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.ARGB_8888; if (DrmHelper.isDrmFile(mLocalFilePath)) { return DecodeUtils.ensureGLCompatibleBitmap(DrmHelper .getBitmap(mLocalFilePath, options)); } int targetSize = MediaItem.getTargetSize(type); // try to decode from JPEG EXIF if (type == MediaItem.TYPE_MICROTHUMBNAIL) { ExifInterface exif = new ExifInterface(); byte[] thumbData = null; try { exif.readExif(mLocalFilePath); thumbData = exif.getThumbnail(); } catch (FileNotFoundException e) { Log.w(TAG, "failed to find file to read thumbnail: " + mLocalFilePath); } catch (IOException e) { Log.w(TAG, "failed to get thumbnail from: " + mLocalFilePath); } if (thumbData != null) { Bitmap bitmap = DecodeUtils.decodeIfBigEnough( jc, thumbData, options, targetSize); if (bitmap != null) return bitmap; } } return DecodeUtils.decodeThumbnail(jc, mLocalFilePath, options, targetSize, type); } } @Override public Job requestLargeImage() { return new LocalLargeImageRequest(filePath); } public static class LocalLargeImageRequest implements Job { String mLocalFilePath; public LocalLargeImageRequest(String localFilePath) { mLocalFilePath = localFilePath; } @Override public BitmapRegionDecoder run(JobContext jc) { if (DrmHelper.isDrmFile(mLocalFilePath)) { return DrmHelper.createBitmapRegionDecoder(mLocalFilePath, false); } return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false); } } @Override public int getSupportedOperations() { int operation = SUPPORT_DELETE | SUPPORT_INFO; if (DrmHelper.isDrmFile(getFilePath())) { operation |= SUPPORT_DRM_INFO | SUPPORT_FULL_IMAGE; if (DrmHelper.isShareableDrmFile(getFilePath())) { operation |= SUPPORT_SHARE; } } else { operation = SUPPORT_SHARE | SUPPORT_CROP | SUPPORT_PRINT | SUPPORT_SETAS; if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) { operation |= SUPPORT_FULL_IMAGE | SUPPORT_EDIT; } if (BitmapUtils.isRotationSupported(mimeType)) { operation |= SUPPORT_ROTATE; } if (GalleryUtils.isValidLocation(latitude, longitude)) { operation |= SUPPORT_SHOW_ON_MAP; } } return operation; } @Override public void getPanoramaSupport(PanoramaSupportCallback callback) { mPanoramaMetadata.getPanoramaSupport(mApplication, callback); } @Override public void clearCachedPanoramaSupport() { mPanoramaMetadata.clearCachedValues(); } @Override public void delete() { GalleryUtils.assertNotInRenderThread(); Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; ContentResolver contentResolver = mApplication.getContentResolver(); SaveImage.deleteAuxFiles(contentResolver, getContentUri()); contentResolver.delete(baseUri, "_id=?", new String[]{String.valueOf(id)}); } @Override public void rotate(int degrees) { GalleryUtils.assertNotInRenderThread(); Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; ContentValues values = new ContentValues(); int rotation = (this.rotation + degrees) % 360; if (rotation < 0) rotation += 360; if (mimeType.equalsIgnoreCase("image/jpeg")) { ExifInterface exifInterface = new ExifInterface(); ExifTag tag = exifInterface.buildTag(ExifInterface.TAG_ORIENTATION, ExifInterface.getOrientationValueForRotation(rotation)); if(tag != null) { exifInterface.setTag(tag); try { exifInterface.forceRewriteExif(filePath); fileSize = new File(filePath).length(); values.put(Images.Media.SIZE, fileSize); } catch (FileNotFoundException e) { Log.w(TAG, "cannot find file to set exif: " + filePath); } catch (IOException e) { Log.w(TAG, "cannot set exif data: " + filePath); } } else { Log.w(TAG, "Could not build tag: " + ExifInterface.TAG_ORIENTATION); } } values.put(Images.Media.ORIENTATION, rotation); mApplication.getContentResolver().update(baseUri, values, "_id=?", new String[]{String.valueOf(id)}); } @Override public Uri getContentUri() { Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; return baseUri.buildUpon().appendPath(String.valueOf(id)).build(); } @Override public int getMediaType() { if (DrmHelper.isDrmFile(getFilePath())) { return MEDIA_TYPE_DRM_IMAGE; } return MEDIA_TYPE_IMAGE; } @Override public MediaDetails getDetails() { MediaDetails details = super.getDetails(); details.addDetail(MediaDetails.INDEX_ORIENTATION, Integer.valueOf(rotation)); if (MIME_TYPE_JPEG.equals(mimeType)) { // ExifInterface returns incorrect values for photos in other format. // For example, the width and height of an webp images is always '0'. MediaDetails.extractExifInfo(details, filePath); } return details; } @Override public int getRotation() { return rotation; } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public String getFilePath() { return filePath; } }