summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/com/android/gallery3d/data/LocalImage.java8
-rw-r--r--src/com/android/gallery3d/filtershow/FilterShowActivity.java29
-rw-r--r--src/com/android/gallery3d/filtershow/cache/ImageLoader.java4
-rw-r--r--src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java288
-rw-r--r--src/com/android/gallery3d/util/SaveVideoFileUtils.java8
5 files changed, 274 insertions, 63 deletions
diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java
index 1ed67ecf4..a7c98af77 100644
--- a/src/com/android/gallery3d/data/LocalImage.java
+++ b/src/com/android/gallery3d/data/LocalImage.java
@@ -35,9 +35,9 @@ import com.android.gallery3d.app.PanoramaMetadataSupport;
import com.android.gallery3d.app.StitchingProgressManager;
import com.android.gallery3d.common.ApiHelper;
import com.android.gallery3d.common.BitmapUtils;
-import com.android.gallery3d.common.Utils;
import com.android.gallery3d.exif.ExifInterface;
import com.android.gallery3d.exif.ExifTag;
+import com.android.gallery3d.filtershow.tools.SaveCopyTask;
import com.android.gallery3d.util.GalleryUtils;
import com.android.gallery3d.util.ThreadPool.Job;
import com.android.gallery3d.util.ThreadPool.JobContext;
@@ -46,8 +46,6 @@ import com.android.gallery3d.util.UpdateHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
-import java.io.RandomAccessFile;
-import java.nio.channels.FileChannel.MapMode;
// LocalImage represents an image in the local storage.
public class LocalImage extends LocalMediaItem {
@@ -271,7 +269,9 @@ public class LocalImage extends LocalMediaItem {
public void delete() {
GalleryUtils.assertNotInRenderThread();
Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI;
- mApplication.getContentResolver().delete(baseUri, "_id=?",
+ ContentResolver contentResolver = mApplication.getContentResolver();
+ SaveCopyTask.deleteAuxFiles(contentResolver, getContentUri());
+ contentResolver.delete(baseUri, "_id=?",
new String[]{String.valueOf(id)});
}
diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
index fbe09b999..13aa3aa85 100644
--- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java
+++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java
@@ -136,6 +136,8 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
private Uri mOriginalImageUri = null;
private ImagePreset mOriginalPreset = null;
+ private Uri mSelectedImageUri = null;
+
private CategoryAdapter mCategoryLooksAdapter = null;
private CategoryAdapter mCategoryBordersAdapter = null;
private CategoryAdapter mCategoryGeometryAdapter = null;
@@ -305,12 +307,13 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
}
mAction = intent.getAction();
- Uri srcUri = intent.getData();
+ mSelectedImageUri = intent.getData();
+ Uri loadUri = mSelectedImageUri;
if (mOriginalImageUri != null) {
- srcUri = mOriginalImageUri;
+ loadUri = mOriginalImageUri;
}
- if (srcUri != null) {
- startLoadBitmap(srcUri);
+ if (loadUri != null) {
+ startLoadBitmap(loadUri);
} else {
pickImage();
}
@@ -907,11 +910,13 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.unsaved).setTitle(R.string.save_before_exit);
builder.setPositiveButton(R.string.save_and_exit, new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int id) {
saveImage();
}
});
builder.setNegativeButton(R.string.exit, new DialogInterface.OnClickListener() {
+ @Override
public void onClick(DialogInterface dialog, int id) {
done();
}
@@ -965,7 +970,7 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
public void saveImage() {
if (mImageShow.hasModifications()) {
// Get the name of the album, to which the image will be saved
- File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mImageLoader.getUri());
+ File saveDir = SaveCopyTask.getFinalSaveDirectory(this, mSelectedImageUri);
int bucketId = GalleryUtils.getBucketId(saveDir.getPath());
String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null);
showSavingProgress(albumName);
@@ -981,10 +986,6 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
finish();
}
- static {
- System.loadLibrary("jni_filtershow_filters");
- }
-
private void extractXMPData() {
XMresults res = XmpPresets.extractXMPData(
getBaseContext(), mMasterImage, getIntent().getData());
@@ -994,4 +995,14 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL
mOriginalImageUri = res.originalimage;
mOriginalPreset = res.preset;
}
+
+ public Uri getSelectedImageUri() {
+ return mSelectedImageUri;
+ }
+
+ static {
+ System.loadLibrary("jni_filtershow_filters");
+ }
+
+
}
diff --git a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
index 7ddd9bebe..491340e0d 100644
--- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
+++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java
@@ -394,7 +394,9 @@ public class ImageLoader {
public void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity,
File destination) {
- new SaveCopyTask(mContext, mUri, destination, new SaveCopyTask.Callback() {
+ Uri selectedImageUri = filterShowActivity.getSelectedImageUri();
+ new SaveCopyTask(mContext, mUri, selectedImageUri, destination,
+ new SaveCopyTask.Callback() {
@Override
public void onComplete(Uri result) {
diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
index 9f7cba30b..0d3d5ac06 100644
--- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
+++ b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java
@@ -25,6 +25,7 @@ import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment;
+import android.provider.MediaStore;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Images.ImageColumns;
import android.util.Log;
@@ -39,6 +40,7 @@ import com.android.gallery3d.util.XmpUtilHelper;
import java.io.File;
import java.io.FileNotFoundException;
+import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Date;
@@ -60,7 +62,7 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
void onComplete(Uri result);
}
- private interface ContentResolverQueryCallback {
+ public interface ContentResolverQueryCallback {
void onCursorResult(Cursor cursor);
}
@@ -69,26 +71,68 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
private static final String PREFIX_PANO = "PANO";
private static final String PREFIX_IMG = "IMG";
private static final String POSTFIX_JPG = ".jpg";
+ private static final String AUX_DIR_NAME = ".aux";
+
+ private final Context mContext;
+ private final Uri mSourceUri;
+ private final Callback mCallback;
+ private final File mDestinationFile;
+ private final Uri mSelectedImageUri;
+
+ // In order to support the new edit-save behavior such that user won't see
+ // the edited image together with the original image, we are adding a new
+ // auxiliary directory for the edited image. Basically, the original image
+ // will be hidden in that directory after edit and user will see the edited
+ // image only.
+ // Note that deletion on the edited image will also cause the deletion of
+ // the original image under auxiliary directory.
+ //
+ // There are several situations we need to consider:
+ // 1. User edit local image local01.jpg. A local02.jpg will be created in the
+ // same directory, and original image will be moved to auxiliary directory as
+ // ./.aux/local02.jpg.
+ // If user edit the local02.jpg, local03.jpg will be created in the local
+ // directory and ./.aux/local02.jpg will be renamed to ./.aux/local03.jpg
+ //
+ // 2. User edit remote image remote01.jpg from picassa or other server.
+ // remoteSavedLocal01.jpg will be saved under proper local directory.
+ // In remoteSavedLocal01.jpg, there will be a reference pointing to the
+ // remote01.jpg. There will be no local copy of remote01.jpg.
+ // If user edit remoteSavedLocal01.jpg, then a new remoteSavedLocal02.jpg
+ // will be generated and still pointing to the remote01.jpg
+ //
+ // 3. User delete any local image local.jpg.
+ // Since the filenames are kept consistent in auxiliary directory, every
+ // time a local.jpg get deleted, the files in auxiliary directory whose
+ // names starting with "local." will be deleted.
+ // This pattern will facilitate the multiple images deletion in the auxiliary
+ // directory.
+ //
+ // TODO: Move the saving into a background service.
- 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;
-
+ /**
+ * @param context
+ * @param sourceUri The Uri for the original image, which can be the hidden
+ * image under the auxiliary directory or the same as selectedImageUri.
+ * @param selectedImageUri The Uri for the image selected by the user.
+ * In most cases, it is a content Uri for local image or remote image.
+ * @param destination Destinaton File, if this is null, a new file will be
+ * created under the same directory as selectedImageUri.
+ * @param callback Let the caller know the saving has completed.
+ * @return the newSourceUri
+ */
+ public SaveCopyTask(Context context, Uri sourceUri, Uri selectedImageUri,
+ File destination, Callback callback) {
+ mContext = context;
+ mSourceUri = sourceUri;
+ mCallback = callback;
if (destination == null) {
- this.destinationFile = getNewFile(context, sourceUri);
+ mDestinationFile = getNewFile(context, selectedImageUri);
} else {
- this.destinationFile = destination;
+ mDestinationFile = destination;
}
- saveFileName = PREFIX_IMG + new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(
- System.currentTimeMillis()));
+ mSelectedImageUri = selectedImageUri;
}
public static File getFinalSaveDirectory(Context context, Uri sourceUri) {
@@ -113,12 +157,64 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
return new File(saveDirectory, PREFIX_IMG + filename + POSTFIX_JPG);
}
+ /**
+ * Remove the files in the auxiliary directory whose names are the same as
+ * the source image.
+ * @param contentResolver The application's contentResolver
+ * @param srcContentUri The content Uri for the source image.
+ */
+ public static void deleteAuxFiles(ContentResolver contentResolver,
+ Uri srcContentUri) {
+ final String[] fullPath = new String[1];
+ String[] queryProjection = new String[] { ImageColumns.DATA };
+ querySourceFromContentResolver(contentResolver,
+ srcContentUri, queryProjection,
+ new ContentResolverQueryCallback() {
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ fullPath[0] = cursor.getString(0);
+ }
+ }
+ );
+ if (fullPath[0] != null) {
+ // Construct the auxiliary directory given the source file's path.
+ // Then select and delete all the files starting with the same name
+ // under the auxiliary directory.
+ File currentFile = new File(fullPath[0]);
+
+ String filename = currentFile.getName();
+ int firstDotPos = filename.indexOf(".");
+ final String filenameNoExt = (firstDotPos == -1) ? filename :
+ filename.substring(0, firstDotPos);
+ File auxDir = getLocalAuxDirectory(currentFile);
+ if (auxDir.exists()) {
+ FilenameFilter filter = new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ if (name.startsWith(filenameNoExt + ".")) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ // Delete all auxiliary files whose name is matching the
+ // current local image.
+ File[] auxFiles = auxDir.listFiles(filter);
+ for (File file : auxFiles) {
+ file.delete();
+ }
+ }
+ }
+ }
+
public Object getPanoramaXMPData(Uri source, ImagePreset preset) {
Object xmp = null;
if (preset.isPanoramaSafe()) {
InputStream is = null;
try {
- is = context.getContentResolver().openInputStream(source);
+ is = mContext.getContentResolver().openInputStream(source);
xmp = XmpUtilHelper.extractXMPMeta(is);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "Failed to get XMP data from image: ", e);
@@ -138,11 +234,11 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
public ExifInterface getExifData(Uri source) {
ExifInterface exif = new ExifInterface();
- String mimeType = context.getContentResolver().getType(sourceUri);
+ String mimeType = mContext.getContentResolver().getType(mSelectedImageUri);
if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) {
InputStream inStream = null;
try {
- inStream = context.getContentResolver().openInputStream(source);
+ inStream = mContext.getContentResolver().openInputStream(source);
exif.readExif(inStream);
} catch (FileNotFoundException e) {
Log.w(LOGTAG, "Cannot find file: " + source, e);
@@ -174,7 +270,7 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
@Override
protected Uri doInBackground(ImagePreset... params) {
// TODO: Support larger dimensions for photo saving.
- if (params[0] == null || sourceUri == null) {
+ if (params[0] == null || mSourceUri == null || mSelectedImageUri == null) {
return null;
}
ImagePreset preset = params[0];
@@ -182,19 +278,25 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
Uri uri = null;
boolean noBitmap = true;
int num_tries = 0;
+
+ // If necessary, move the source file into the auxiliary directory,
+ // newSourceUri is then pointing to the new location.
+ // If no file is moved, newSourceUri will be the same as mSourceUri.
+ Uri newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile);
+
// Stopgap fix for low-memory devices.
while (noBitmap) {
try {
// Try to do bitmap operations, downsample if low-memory
- Bitmap bitmap = ImageLoader.loadMutableBitmap(context, sourceUri, options);
+ Bitmap bitmap = ImageLoader.loadMutableBitmap(mContext, newSourceUri, options);
if (bitmap == null) {
return null;
}
CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Saving");
bitmap = pipeline.renderFinalImage(bitmap, preset);
- Object xmp = getPanoramaXMPData(sourceUri, preset);
- ExifInterface exif = getExifData(sourceUri);
+ Object xmp = getPanoramaXMPData(mSelectedImageUri, preset);
+ ExifInterface exif = getExifData(mSelectedImageUri);
// Set tags
long time = System.currentTimeMillis();
@@ -207,12 +309,23 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
exif.removeCompressedThumbnail();
// If we succeed in writing the bitmap as a jpeg, return a uri.
- if (putExifData(this.destinationFile, exif, bitmap)) {
- putPanoramaXMPData(this.destinationFile, xmp);
- uri = insertContent(context, sourceUri, this.destinationFile, saveFileName,
+ if (putExifData(mDestinationFile, exif, bitmap)) {
+ putPanoramaXMPData(mDestinationFile, xmp);
+ uri = insertContent(mContext, mSelectedImageUri, mDestinationFile,
time);
}
- XmpPresets.writeFilterXMP(context, sourceUri, this.destinationFile, preset);
+
+ // mDestinationFile will save the newSourceUri info in the XMP.
+ XmpPresets.writeFilterXMP(mContext, newSourceUri, mDestinationFile, preset);
+
+ // Since we have a new image inserted to media store, we can
+ // safely remove the old one which is selected by the user.
+ String scheme = mSelectedImageUri.getScheme();
+ if (scheme != null && scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+ if (mSelectedImageUri.getAuthority().equals(MediaStore.AUTHORITY)) {
+ mContext.getContentResolver().delete(mSelectedImageUri, null, null);
+ }
+ }
noBitmap = false;
} catch (java.lang.OutOfMemoryError e) {
@@ -227,17 +340,73 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
return uri;
}
+ /**
+ * Move the source file to auxiliary directory if needed and return the Uri
+ * pointing to this new source file.
+ * @param srcUri Uri to the source image.
+ * @param dstFile Providing the destination file info to help to build the
+ * auxiliary directory and new source file's name.
+ * @return the newSourceUri pointing to the new source image.
+ */
+ private Uri moveSrcToAuxIfNeeded(Uri srcUri, File dstFile) {
+ File srcFile = getFileFromUri(mContext, srcUri);
+ if (srcFile == null) {
+ Log.d(LOGTAG, "Source file is not a local file, no update.");
+ return srcUri;
+ }
+
+ // Get the destination directory and create the auxilliary directory
+ // if necessary.
+ File auxDiretory = getLocalAuxDirectory(dstFile);
+ if (!auxDiretory.exists()) {
+ auxDiretory.mkdirs();
+ }
+
+ // Make sure there is a .nomedia file in the auxiliary directory, such
+ // that MediaScanner will not report those files under this directory.
+ File noMedia = new File(auxDiretory, ".nomedia");
+ if (!noMedia.exists()) {
+ try {
+ noMedia.createNewFile();
+ } catch (IOException e) {
+ Log.e(LOGTAG, "Can't create the nomedia");
+ return srcUri;
+ }
+ }
+ // We are using the destination file name such that photos sitting in
+ // the auxiliary directory are matching the parent directory.
+ File newSrcFile = new File(auxDiretory, dstFile.getName());
+
+ if (!newSrcFile.exists()) {
+ srcFile.renameTo(newSrcFile);
+ }
+
+ return Uri.fromFile(newSrcFile);
+
+ }
+
+ private static File getLocalAuxDirectory(File dstFile) {
+ File dstDirectory = dstFile.getParentFile();
+ File auxDiretory = new File(dstDirectory + "/" + AUX_DIR_NAME);
+ return auxDiretory;
+ }
@Override
protected void onPostExecute(Uri result) {
- if (callback != null) {
- callback.onComplete(result);
+ if (mCallback != null) {
+ mCallback.onComplete(result);
}
}
private static void querySource(Context context, Uri sourceUri, String[] projection,
ContentResolverQueryCallback callback) {
ContentResolver contentResolver = context.getContentResolver();
+ querySourceFromContentResolver(contentResolver, sourceUri, projection, callback);
+ }
+
+ private static void querySourceFromContentResolver(
+ ContentResolver contentResolver, Uri sourceUri, String[] projection,
+ ContentResolverQueryCallback callback) {
Cursor cursor = null;
try {
cursor = contentResolver.query(sourceUri, projection, null, null,
@@ -255,18 +424,51 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
}
private static File getSaveDirectory(Context context, Uri sourceUri) {
- final File[] dir = new File[1];
- querySource(context, sourceUri, new String[] {
- ImageColumns.DATA
- },
- new ContentResolverQueryCallback() {
+ File file = getFileFromUri(context, sourceUri);
+ if (file != null) {
+ return file.getParentFile();
+ } else {
+ return null;
+ }
+ }
- @Override
- public void onCursorResult(Cursor cursor) {
- dir[0] = new File(cursor.getString(0)).getParentFile();
- }
- });
- return dir[0];
+ /**
+ * Construct a File object based on the srcUri.
+ * @return The file object. Return null if srcUri is invalid or not a local
+ * file.
+ */
+ private static File getFileFromUri(Context context, Uri srcUri) {
+ if (srcUri == null) {
+ Log.e(LOGTAG, "srcUri is null.");
+ return null;
+ }
+
+ String scheme = srcUri.getScheme();
+ if (scheme == null) {
+ Log.e(LOGTAG, "scheme is null.");
+ return null;
+ }
+
+ final File[] file = new File[1];
+ // sourceUri can be a file path or a content Uri, it need to be handled
+ // differently.
+ if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+ if (srcUri.getAuthority().equals(MediaStore.AUTHORITY)) {
+ querySource(context, srcUri, new String[] {
+ ImageColumns.DATA
+ },
+ new ContentResolverQueryCallback() {
+
+ @Override
+ public void onCursorResult(Cursor cursor) {
+ file[0] = new File(cursor.getString(0));
+ }
+ });
+ }
+ } else if (scheme.equals(ContentResolver.SCHEME_FILE)) {
+ file[0] = new File(srcUri.getPath());
+ }
+ return file[0];
}
/**
@@ -299,16 +501,16 @@ public class SaveCopyTask extends AsyncTask<ImagePreset, Void, Uri> {
/**
* Insert the content (saved file) with proper source photo properties.
*/
- public static Uri insertContent(Context context, Uri sourceUri, File file, String saveFileName,
+ private static Uri insertContent(Context context, Uri sourceUri, File file,
long time) {
time /= 1000;
final ContentValues values = new ContentValues();
- values.put(Images.Media.TITLE, saveFileName);
+ values.put(Images.Media.TITLE, file.getName());
values.put(Images.Media.DISPLAY_NAME, file.getName());
values.put(Images.Media.MIME_TYPE, "image/jpeg");
values.put(Images.Media.DATE_TAKEN, time);
- values.put(Images.Media.DATE_MODIFIED, time);
+ values.put(Images.Media.DATE_MODIFIED, System.currentTimeMillis());
values.put(Images.Media.DATE_ADDED, time);
values.put(Images.Media.ORIENTATION, 0);
values.put(Images.Media.DATA, file.getAbsolutePath());
diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
index e2c5f51b9..da0970b1d 100644
--- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java
+++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java
@@ -25,17 +25,13 @@ import android.os.Environment;
import android.provider.MediaStore.Video;
import android.provider.MediaStore.Video.VideoColumns;
+import com.android.gallery3d.filtershow.tools.SaveCopyTask.ContentResolverQueryCallback;
+
import java.io.File;
import java.sql.Date;
import java.text.SimpleDateFormat;
public class SaveVideoFileUtils {
- // Copy from SaveCopyTask.java in terms of how to handle the destination
- // path and filename : querySource() and getSaveDirectory().
- public interface ContentResolverQueryCallback {
- void onCursorResult(Cursor cursor);
- }
-
// This function can decide which folder to save the video file, and generate
// the needed information for the video file including filename.
public static SaveVideoFileInfo getDstMp4FileInfo(String fileNameFormat,