blob: 9c55623d1342e09d8c05e1d8f301f3849ed908d3 [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.filtershow.tools;
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.Bitmap.CompressFormat;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;
import com.android.gallery3d.filtershow.cache.ImageLoader;
import com.android.gallery3d.filtershow.presets.ImagePreset;
import com.android.gallery3d.util.XmpUtilHelper;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Date;
import java.text.SimpleDateFormat;
/**
* Asynchronous task for saving edited photo as a new copy.
*/
public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
private static final String LOGTAG = "SaveCopyTask";
private static final int DEFAULT_COMPRESS_QUALITY = 95;
private static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos";
/**
* Saves the bitmap in the final destination
*/
public static void saveBitmap(Bitmap bitmap, File destination, Object xmp) {
OutputStream os = null;
try {
os = new FileOutputStream(destination);
bitmap.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, os);
} catch (FileNotFoundException e) {
Log.v(LOGTAG,"Error in writing "+destination.getAbsolutePath());
} finally {
closeStream(os);
}
if (xmp != null) {
XmpUtilHelper.writeXMPMeta(destination.getAbsolutePath(), xmp);
}
}
private static void closeStream(Closeable stream) {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Callback for the completed asynchronous task.
*/
public interface Callback {
void onComplete(Uri result);
}
private interface ContentResolverQueryCallback {
void onCursorResult(Cursor cursor);
}
private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
private final Context context;
private final Uri sourceUri;
private final Callback callback;
private final String saveFileName;
private final File destinationFile;
public SaveCopyTask(Context context, Uri sourceUri, File destination, Callback callback) {
this.context = context;
this.sourceUri = sourceUri;
this.callback = callback;
if (destination == null) {
this.destinationFile = getNewFile(context, sourceUri);
} else {
this.destinationFile = destination;
}
saveFileName = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
System.currentTimeMillis()));
}
public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
File saveDirectory = getSaveDirectory(context, sourceUri);
if ((saveDirectory == null) || !saveDirectory.canWrite()) {
saveDirectory = new File(Environment.getExternalStorageDirectory(),
DEFAULT_SAVE_DIRECTORY);
}
// Create the directory if it doesn't exist
if (!saveDirectory.exists()) saveDirectory.mkdirs();
return saveDirectory;
}
public static File getNewFile(Context context, Uri sourceUri) {
File saveDirectory = getFinalSaveDirectory(context, sourceUri);
String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
System.currentTimeMillis()));
return new File(saveDirectory, filename + ".JPG");
}
private Bitmap loadMutableBitmap() throws FileNotFoundException {
BitmapFactory.Options options = new BitmapFactory.Options();
// TODO: on <3.x we need a copy of the bitmap (inMutable doesn't
// exist)
options.inMutable = true;
InputStream is = context.getContentResolver().openInputStream(sourceUri);
Bitmap bitmap = BitmapFactory.decodeStream(is, null, options);
int orientation = ImageLoader.getOrientation(context, sourceUri);
bitmap = ImageLoader.rotateToPortrait(bitmap, orientation);
return bitmap;
}
private static final String[] COPY_EXIF_ATTRIBUTES = new String[] {
ExifInterface.TAG_APERTURE,
ExifInterface.TAG_DATETIME,
ExifInterface.TAG_EXPOSURE_TIME,
ExifInterface.TAG_FLASH,
ExifInterface.TAG_FOCAL_LENGTH,
ExifInterface.TAG_GPS_ALTITUDE,
ExifInterface.TAG_GPS_ALTITUDE_REF,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_GPS_LATITUDE,
ExifInterface.TAG_GPS_LATITUDE_REF,
ExifInterface.TAG_GPS_LONGITUDE,
ExifInterface.TAG_GPS_LONGITUDE_REF,
ExifInterface.TAG_GPS_PROCESSING_METHOD,
ExifInterface.TAG_GPS_DATESTAMP,
ExifInterface.TAG_ISO,
ExifInterface.TAG_MAKE,
ExifInterface.TAG_MODEL,
ExifInterface.TAG_WHITE_BALANCE,
};
private static void copyExif(String sourcePath, String destPath) {
try {
ExifInterface source = new ExifInterface(sourcePath);
ExifInterface dest = new ExifInterface(destPath);
boolean needsSave = false;
for (String tag : COPY_EXIF_ATTRIBUTES) {
String value = source.getAttribute(tag);
if (value != null) {
needsSave = true;
dest.setAttribute(tag, value);
}
}
if (needsSave) {
dest.saveAttributes();
}
} catch (IOException ex) {
Log.w(LOGTAG, "Failed to copy exif metadata", ex);
}
}
private void copyExif(Uri sourceUri, String destPath) {
if (ContentResolver.SCHEME_FILE.equals(sourceUri.getScheme())) {
copyExif(sourceUri.getPath(), destPath);
return;
}
final String[] PROJECTION = new String[] {
ImageColumns.DATA
};
try {
Cursor c = context.getContentResolver().query(sourceUri, PROJECTION,
null, null, null);
if (c.moveToFirst()) {
String path = c.getString(0);
if (new File(path).exists()) {
copyExif(path, destPath);
}
}
c.close();
} catch (Exception e) {
Log.w(LOGTAG, "Failed to copy exif", e);
}
}
/**
* The task should be executed with one given bitmap to be saved.
*/
@Override
protected Uri doInBackground(ImagePreset... params) {
// TODO: Support larger dimensions for photo saving.
if (params[0] == null) {
return null;
}
ImagePreset preset = params[0];
try {
Bitmap bitmap = preset.apply(loadMutableBitmap());
Object xmp = null;
InputStream is = null;
if (preset.isPanoramaSafe()) {
is = context.getContentResolver().openInputStream(sourceUri);
xmp = XmpUtilHelper.extractXMPMeta(is);
}
saveBitmap(bitmap, this.destinationFile, xmp);
copyExif(sourceUri, destinationFile.getAbsolutePath());
Uri uri = insertContent(context, sourceUri, this.destinationFile, saveFileName);
bitmap.recycle();
return uri;
} catch (FileNotFoundException ex) {
Log.w(LOGTAG, "Failed to save image!", ex);
return null;
}
}
@Override
protected void onPostExecute(Uri result) {
if (callback != null) {
callback.onComplete(result);
}
}
private static void querySource(Context context, Uri sourceUri, String[] projection,
ContentResolverQueryCallback callback) {
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = null;
try {
cursor = contentResolver.query(sourceUri, projection, null, null,
null);
if ((cursor != null) && cursor.moveToNext()) {
callback.onCursorResult(cursor);
}
} catch (Exception e) {
// Ignore error for lacking the data column from the source.
} finally {
if (cursor != null) {
cursor.close();
}
}
}
private static File getSaveDirectory(Context context, Uri sourceUri) {
final File[] dir = new File[1];
querySource(context, sourceUri, new String[] {
ImageColumns.DATA
},
new ContentResolverQueryCallback() {
@Override
public void onCursorResult(Cursor cursor) {
dir[0] = new File(cursor.getString(0)).getParentFile();
}
});
return dir[0];
}
/**
* Insert the content (saved file) with proper source photo properties.
*/
public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName) {
long now = System.currentTimeMillis() / 1000;
final ContentValues values = new ContentValues();
values.put(Images.Media.TITLE, saveFileName);
values.put(Images.Media.DISPLAY_NAME, file.getName());
values.put(Images.Media.MIME_TYPE, "image/jpeg");
values.put(Images.Media.DATE_TAKEN, now);
values.put(Images.Media.DATE_MODIFIED, now);
values.put(Images.Media.DATE_ADDED, now);
values.put(Images.Media.ORIENTATION, 0);
values.put(Images.Media.DATA, file.getAbsolutePath());
values.put(Images.Media.SIZE, file.length());
final String[] projection = new String[] {
ImageColumns.DATE_TAKEN,
ImageColumns.LATITUDE, ImageColumns.LONGITUDE,
};
querySource(context, sourceUri, projection,
new ContentResolverQueryCallback() {
@Override
public void onCursorResult(Cursor cursor) {
values.put(Images.Media.DATE_TAKEN, cursor.getLong(0));
double latitude = cursor.getDouble(1);
double longitude = cursor.getDouble(2);
// TODO: Change || to && after the default location issue is
// fixed.
if ((latitude != 0f) || (longitude != 0f)) {
values.put(Images.Media.LATITUDE, latitude);
values.put(Images.Media.LONGITUDE, longitude);
}
}
});
return context.getContentResolver().insert(
Images.Media.EXTERNAL_CONTENT_URI, values);
}
}