blob: 2d60a4759568de3fc5fcb06ccc4f98a86d140b83 [file] [log] [blame]
/*
* Copyright (C) 2009 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.camera.gallery;
import com.android.camera.BitmapManager;
import com.android.camera.ExifInterface;
import com.android.camera.Util;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.BaseColumns;
import android.provider.MediaStore.Images.Thumbnails;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
* The class for normal images in gallery.
*/
public class Image extends BaseImage implements IImage {
private static final String TAG = "BaseImage";
private int mRotation;
private ExifInterface mExif;
public Image(long id, long miniThumbMagic, ContentResolver cr,
BaseImageList container, int cursorRow, int rotation) {
super(id, miniThumbMagic, cr, container, cursorRow);
mRotation = rotation;
}
public String getDataPath() {
String path = null;
Cursor c = getCursor();
synchronized (c) {
if (c.moveToPosition(getRow())) {
int column = ((ImageList) getContainer()).indexData();
if (column >= 0)
path = c.getString(column);
}
}
return path;
}
@Override
protected int getDegreesRotated() {
return mRotation;
}
protected void setDegreesRotated(int degrees) {
Cursor c = getCursor();
mRotation = degrees;
synchronized (c) {
if (c.moveToPosition(getRow())) {
int column = ((ImageList) getContainer()).indexOrientation();
if (column >= 0) {
c.updateInt(column, degrees);
c.commitUpdates();
}
}
}
}
@Override
protected Bitmap.CompressFormat compressionType() {
String mimeType = getMimeType();
if ("image/png".equals(mimeType)) {
return Bitmap.CompressFormat.PNG;
} else if ("image/gif".equals(mimeType)) {
return Bitmap.CompressFormat.PNG;
}
return Bitmap.CompressFormat.JPEG;
}
/**
* Does not replace the tag if already there. Otherwise, adds to the exif
* tags.
*
* @param tag
* @param value
*/
public void addExifTag(String tag, String value) {
if (mExifData == null) {
loadExifData();
}
// If the key is already there, ignore it.
if (!mExifData.containsKey(tag)) {
mExifData.put(tag, value);
}
}
/**
* Return the value of the Exif tag as an int. Returns 0 on any type of
* error.
*
* @param tag
*/
public int getExifTagInt(String tag, int defaultValue) {
String tagValue = getExifTag(tag);
try {
if (tagValue != null) {
return Integer.parseInt(tagValue);
}
} catch (NumberFormatException ex) {
// Simply return defaultValue if exception is thrown.
Log.v(TAG, ex.toString());
}
return defaultValue;
}
/**
* Return the value of the Exif tag as a String. It's caller's
* responsibility to check nullity.
*/
public String getExifTag(String tag) {
if (mExifData == null) {
loadExifData();
}
return mExifData.get(tag);
}
public boolean isReadonly() {
String mimeType = getMimeType();
return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType);
}
public boolean isDrm() {
return false;
}
/**
* Remove tag if already there. Otherwise, does nothing.
* @param tag
*/
public void removeExifTag(String tag) {
if (mExifData == null) {
loadExifData();
}
mExifData.remove(tag);
}
/**
* Replaces the tag if already there. Otherwise, adds to the exif tags.
* @param tag
* @param value
*/
public void replaceExifTag(String tag, String value) {
if (mExifData == null) {
loadExifData();
}
mExifData.put(tag, value);
}
private class SaveImageContentsCancelable extends BaseCancelable<Boolean> {
private final Bitmap mImage;
private final byte [] mJpegData;
private final int mOrientation;
private final String mFilePath;
ICancelable<Boolean> mCurrentCancelable = null;
SaveImageContentsCancelable(Bitmap image, byte[] jpegData,
int orientation, String filePath) {
mImage = image;
mJpegData = jpegData;
mOrientation = orientation;
mFilePath = filePath;
}
@Override
public boolean doCancelWork() {
synchronized (this) {
if (mCurrentCancelable != null) mCurrentCancelable.cancel();
}
return true;
}
public Boolean get() {
try {
Bitmap thumbnail = null;
Uri uri = mContainer.contentUri(mId);
synchronized (this) {
checkCanceled();
mCurrentCancelable =
compressImageToFile(mImage, mJpegData, uri);
}
if (!mCurrentCancelable.get()) return false;
synchronized (this) {
String filePath = mFilePath;
// TODO: If thumbData is present and usable, we should call
// the version of storeThumbnail which takes a byte array,
// rather than re-encoding a new JPEG of the same
// dimensions.
byte[] thumbData = null;
synchronized (ExifInterface.class) {
thumbData =
(new ExifInterface(filePath)).getThumbnail();
}
if (thumbData != null) {
thumbnail = BitmapFactory.decodeByteArray(
thumbData, 0, thumbData.length);
}
if (thumbnail == null && mImage != null) {
thumbnail = mImage;
}
if (thumbnail == null && mJpegData != null) {
thumbnail = BitmapFactory.decodeByteArray(
mJpegData, 0, mJpegData.length);
}
}
mContainer.storeThumbnail(
thumbnail, Image.this.fullSizeImageId());
checkCanceled();
try {
thumbnail = Util.rotate(thumbnail, mOrientation);
saveMiniThumb(thumbnail);
} catch (IOException e) {
// Ignore if unable to save thumb.
}
checkCanceled();
return true;
} catch (CanceledException ex) {
// Got canceled... need to cleanup.
return false;
} finally {
/*
* Cursor c = getCursor(); synchronized (c) { if
* (c.moveTo(getRow())) { mContainer.requery(); } }
*/
acknowledgeCancel();
}
}
}
public ICancelable<Boolean> saveImageContents(Bitmap image,
byte [] jpegData, int orientation, boolean newFile,
String filePath) {
return new SaveImageContentsCancelable(
image, jpegData, orientation, filePath);
}
private void loadExifData() {
Cursor c = getCursor();
String filePath;
synchronized (c) {
filePath = c.getString(mContainer.indexData());
}
ExifInterface mExif = new ExifInterface(filePath);
if (mExifData == null) {
mExifData = mExif.getAttributes();
}
}
private void saveExifData() {
if (mExif != null && mExifData != null) {
mExif.saveAttributes(mExifData);
}
}
private void setExifRotation(int degrees) {
try {
if (degrees < 0) degrees += 360;
int orientation = ExifInterface.ORIENTATION_NORMAL;
switch (degrees) {
case 0:
orientation = ExifInterface.ORIENTATION_NORMAL;
break;
case 90:
orientation = ExifInterface.ORIENTATION_ROTATE_90;
break;
case 180:
orientation = ExifInterface.ORIENTATION_ROTATE_180;
break;
case 270:
orientation = ExifInterface.ORIENTATION_ROTATE_270;
break;
}
replaceExifTag(ExifInterface.TAG_ORIENTATION,
Integer.toString(orientation));
replaceExifTag("UserComment",
"saveRotatedImage comment orientation: " + orientation);
saveExifData();
} catch (RuntimeException ex) {
Log.e(TAG, "unable to save exif data with new orientation "
+ fullSizeImageUri());
}
}
/**
* Save the rotated image by updating the Exif "Orientation" tag.
* @param degrees
*/
public boolean rotateImageBy(int degrees) {
int newDegrees = getDegreesRotated() + degrees;
setExifRotation(newDegrees);
setDegreesRotated(newDegrees);
// setting this to zero will force the call to checkCursor to generate
// fresh thumbs
mMiniThumbMagic = 0;
try {
mContainer.checkThumbnail(this, this.getRow(), null);
} catch (IOException e) {
// Ignore inability to store mini thumbnail.
}
return true;
}
private static final String[] THUMB_PROJECTION = new String[] {
BaseColumns._ID,
};
public Bitmap thumbBitmap() {
Bitmap bitmap = null;
if (mContainer.mThumbUri != null) {
Cursor c = mContentResolver.query(
mContainer.mThumbUri, THUMB_PROJECTION,
Thumbnails.IMAGE_ID + "=?",
new String[] { String.valueOf(fullSizeImageId()) },
null);
try {
if (c.moveToFirst()) {
bitmap = decodeCurrentImage(c.getLong(0));
}
} catch (RuntimeException ex) {
// sdcard removed?
return null;
} finally {
c.close();
}
}
if (bitmap == null) {
bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
// No thumbnail found... storing the new one.
bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
}
if (bitmap != null) {
bitmap = Util.rotate(bitmap, getDegreesRotated());
}
long elapsed = System.currentTimeMillis();
return bitmap;
}
private Bitmap decodeCurrentImage(long id) {
Uri thumbUri = ContentUris.withAppendedId(
mContainer.mThumbUri, id);
ParcelFileDescriptor pfdInput;
Bitmap bitmap = null;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
bitmap = BitmapManager.instance().decodeFileDescriptor(
pfdInput.getFileDescriptor(), options);
pfdInput.close();
} catch (FileNotFoundException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (IOException ex) {
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (NullPointerException ex) {
// we seem to get this if the file doesn't exist anymore
Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
} catch (OutOfMemoryError ex) {
Log.e(TAG, "failed to allocate memory for thumbnail "
+ thumbUri + "; " + ex);
}
return bitmap;
}
}