blob: c19649c3ea66c547110f0ed4e519c8e71270df94 [file] [log] [blame]
/*
* 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;
}
}