| /* |
| * 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.content.Context; |
| import android.database.Cursor; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.BitmapRegionDecoder; |
| import android.media.ExifInterface; |
| 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.common.ApiHelper; |
| import com.android.gallery3d.common.BitmapUtils; |
| import com.android.gallery3d.util.GalleryUtils; |
| import com.android.gallery3d.util.LightCycleHelper; |
| 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.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 boolean mUsePanoramaViewer; |
| private boolean mUsePanoramaViewerInitialized; |
| |
| 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<Bitmap> requestImage(int type) { |
| return new LocalImageRequest(mApplication, mPath, type, filePath); |
| } |
| |
| public static class LocalImageRequest extends ImageCacheRequest { |
| private String mLocalFilePath; |
| |
| LocalImageRequest(GalleryApp application, Path path, int type, |
| String localFilePath) { |
| super(application, path, type, MediaItem.getTargetSize(type)); |
| mLocalFilePath = localFilePath; |
| } |
| |
| @Override |
| public Bitmap onDecodeOriginal(JobContext jc, final int type) { |
| BitmapFactory.Options options = new BitmapFactory.Options(); |
| options.inPreferredConfig = Bitmap.Config.ARGB_8888; |
| int targetSize = MediaItem.getTargetSize(type); |
| |
| // try to decode from JPEG EXIF |
| if (type == MediaItem.TYPE_MICROTHUMBNAIL) { |
| ExifInterface exif = null; |
| byte [] thumbData = null; |
| try { |
| exif = new ExifInterface(mLocalFilePath); |
| if (exif != null) { |
| thumbData = exif.getThumbnail(); |
| } |
| } catch (Throwable t) { |
| Log.w(TAG, "fail to get exif thumb", t); |
| } |
| 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<BitmapRegionDecoder> requestLargeImage() { |
| return new LocalLargeImageRequest(filePath); |
| } |
| |
| public static class LocalLargeImageRequest |
| implements Job<BitmapRegionDecoder> { |
| String mLocalFilePath; |
| |
| public LocalLargeImageRequest(String localFilePath) { |
| mLocalFilePath = localFilePath; |
| } |
| |
| @Override |
| public BitmapRegionDecoder run(JobContext jc) { |
| return DecodeUtils.createBitmapRegionDecoder(jc, mLocalFilePath, false); |
| } |
| } |
| |
| @Override |
| public int getSupportedOperations() { |
| int operation = SUPPORT_DELETE | SUPPORT_SHARE | SUPPORT_CROP |
| | SUPPORT_SETAS | SUPPORT_EDIT | SUPPORT_INFO; |
| if (BitmapUtils.isSupportedByRegionDecoder(mimeType)) { |
| operation |= SUPPORT_FULL_IMAGE; |
| } |
| |
| if (BitmapUtils.isRotationSupported(mimeType)) { |
| operation |= SUPPORT_ROTATE; |
| } |
| |
| if (GalleryUtils.isValidLocation(latitude, longitude)) { |
| operation |= SUPPORT_SHOW_ON_MAP; |
| } |
| |
| if (usePanoramaViewer()) { |
| operation |= SUPPORT_PANORAMA; |
| } |
| return operation; |
| } |
| |
| @Override |
| public void delete() { |
| GalleryUtils.assertNotInRenderThread(); |
| Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; |
| mApplication.getContentResolver().delete(baseUri, "_id=?", |
| new String[]{String.valueOf(id)}); |
| mApplication.getDataManager().broadcastLocalDeletion(); |
| } |
| |
| private static String getExifOrientation(int orientation) { |
| switch (orientation) { |
| case 0: |
| return String.valueOf(ExifInterface.ORIENTATION_NORMAL); |
| case 90: |
| return String.valueOf(ExifInterface.ORIENTATION_ROTATE_90); |
| case 180: |
| return String.valueOf(ExifInterface.ORIENTATION_ROTATE_180); |
| case 270: |
| return String.valueOf(ExifInterface.ORIENTATION_ROTATE_270); |
| default: |
| throw new AssertionError("invalid: " + orientation); |
| } |
| } |
| |
| @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")) { |
| try { |
| ExifInterface exif = new ExifInterface(filePath); |
| exif.setAttribute(ExifInterface.TAG_ORIENTATION, |
| getExifOrientation(rotation)); |
| exif.saveAttributes(); |
| } catch (IOException e) { |
| Log.w(TAG, "cannot set exif data: " + filePath); |
| } |
| |
| // We need to update the filesize as well |
| fileSize = new File(filePath).length(); |
| values.put(Images.Media.SIZE, fileSize); |
| } |
| |
| 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() { |
| 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; |
| } |
| |
| @Override |
| public boolean usePanoramaViewer() { |
| if (!mUsePanoramaViewerInitialized) { |
| Context context = mApplication.getAndroidContext(); |
| mUsePanoramaViewer = LightCycleHelper.hasLightCycleView(context) |
| && LightCycleHelper.isPanorama(mApplication.getContentResolver(), |
| getContentUri()); |
| mUsePanoramaViewerInitialized = true; |
| } |
| return mUsePanoramaViewer; |
| } |
| } |