From cf76723e40171afef891d494d4c537200597acbb Mon Sep 17 00:00:00 2001 From: nicolasroard Date: Fri, 12 Jul 2013 18:27:54 -0700 Subject: Add background processing service bug:7298624 Change-Id: Ie79f88fd84fdf8f4dab6a8071f06a819e247b357 --- src/com/android/gallery3d/data/LocalImage.java | 4 +- .../gallery3d/filtershow/FilterShowActivity.java | 215 ++++--- .../gallery3d/filtershow/cache/ImageLoader.java | 22 +- .../gallery3d/filtershow/crop/CropActivity.java | 6 +- .../filtershow/filters/BaseFiltersManager.java | 108 +++- .../gallery3d/filtershow/imageshow/ImageShow.java | 4 +- .../filtershow/imageshow/MasterImage.java | 3 + .../filtershow/pipeline/CachingPipeline.java | 4 +- .../filtershow/pipeline/FilteringPipeline.java | 1 - .../gallery3d/filtershow/pipeline/ImagePreset.java | 4 +- .../filtershow/pipeline/ImageSavingTask.java | 123 ++++ .../filtershow/pipeline/ProcessingService.java | 217 +++++++ .../filtershow/pipeline/ProcessingTask.java | 72 +++ .../pipeline/ProcessingTaskController.java | 98 ++++ .../gallery3d/filtershow/tools/SaveCopyTask.java | 611 -------------------- .../gallery3d/filtershow/tools/SaveImage.java | 628 +++++++++++++++++++++ .../android/gallery3d/util/SaveVideoFileUtils.java | 2 +- 17 files changed, 1349 insertions(+), 773 deletions(-) create mode 100644 src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java create mode 100644 src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java create mode 100644 src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java create mode 100644 src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java delete mode 100644 src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java create mode 100644 src/com/android/gallery3d/filtershow/tools/SaveImage.java (limited to 'src') diff --git a/src/com/android/gallery3d/data/LocalImage.java b/src/com/android/gallery3d/data/LocalImage.java index a7c98af77..eb1efae66 100644 --- a/src/com/android/gallery3d/data/LocalImage.java +++ b/src/com/android/gallery3d/data/LocalImage.java @@ -37,7 +37,7 @@ import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.common.BitmapUtils; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; -import com.android.gallery3d.filtershow.tools.SaveCopyTask; +import com.android.gallery3d.filtershow.tools.SaveImage; import com.android.gallery3d.util.GalleryUtils; import com.android.gallery3d.util.ThreadPool.Job; import com.android.gallery3d.util.ThreadPool.JobContext; @@ -270,7 +270,7 @@ public class LocalImage extends LocalMediaItem { GalleryUtils.assertNotInRenderThread(); Uri baseUri = Images.Media.EXTERNAL_CONTENT_URI; ContentResolver contentResolver = mApplication.getContentResolver(); - SaveCopyTask.deleteAuxFiles(contentResolver, getContentUri()); + SaveImage.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 bee764b25..41d178411 100644 --- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -19,28 +19,29 @@ package com.android.gallery3d.filtershow; import android.app.ActionBar; import android.app.AlertDialog; import android.app.ProgressDialog; +import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.IBinder; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentTransaction; import android.util.DisplayMetrics; import android.util.Log; import android.util.TypedValue; -import android.view.Display; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -55,20 +56,19 @@ import android.widget.ShareActionProvider.OnShareTargetSelectedListener; import android.widget.Toast; import com.android.gallery3d.R; +import com.android.gallery3d.app.PhotoPage; import com.android.gallery3d.data.LocalAlbum; import com.android.gallery3d.filtershow.pipeline.CachingPipeline; import com.android.gallery3d.filtershow.pipeline.FilteringPipeline; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.category.Action; import com.android.gallery3d.filtershow.category.CategoryAdapter; -import com.android.gallery3d.filtershow.category.CategoryView; import com.android.gallery3d.filtershow.category.MainPanel; import com.android.gallery3d.filtershow.editors.BasicEditor; import com.android.gallery3d.filtershow.editors.Editor; import com.android.gallery3d.filtershow.editors.EditorCrop; import com.android.gallery3d.filtershow.editors.EditorDraw; import com.android.gallery3d.filtershow.editors.EditorFlip; -import com.android.gallery3d.filtershow.editors.EditorInfo; import com.android.gallery3d.filtershow.editors.EditorManager; import com.android.gallery3d.filtershow.editors.EditorPanel; import com.android.gallery3d.filtershow.editors.EditorRedEye; @@ -76,8 +76,6 @@ import com.android.gallery3d.filtershow.editors.EditorRotate; import com.android.gallery3d.filtershow.editors.EditorStraighten; import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; -import com.android.gallery3d.filtershow.filters.FilterFxRepresentation; -import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation; import com.android.gallery3d.filtershow.filters.FilterRepresentation; import com.android.gallery3d.filtershow.filters.FiltersManager; import com.android.gallery3d.filtershow.filters.ImageFilter; @@ -88,9 +86,10 @@ import com.android.gallery3d.filtershow.imageshow.ImageCrop; import com.android.gallery3d.filtershow.imageshow.ImageShow; import com.android.gallery3d.filtershow.imageshow.MasterImage; import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.ProcessingService; import com.android.gallery3d.filtershow.provider.SharedImageProvider; import com.android.gallery3d.filtershow.state.StateAdapter; -import com.android.gallery3d.filtershow.tools.SaveCopyTask; +import com.android.gallery3d.filtershow.tools.SaveImage; import com.android.gallery3d.filtershow.tools.XmpPresets; import com.android.gallery3d.filtershow.tools.XmpPresets.XMresults; import com.android.gallery3d.filtershow.ui.FramedTextButton; @@ -101,6 +100,7 @@ import com.android.photos.data.GalleryBitmapPool; import java.io.File; import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.Vector; public class FilterShowActivity extends FragmentActivity implements OnItemClickListener, @@ -149,6 +149,75 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL private CategoryAdapter mCategoryFiltersAdapter = null; private int mCurrentPanel = MainPanel.LOOKS; + private ProcessingService mBoundService; + private boolean mIsBound = false; + + public ProcessingService getProcessingService() { + return mBoundService; + } + + public boolean isSimpleEditAction() { + return !PhotoPage.ACTION_NEXTGEN_EDIT.equalsIgnoreCase(mAction); + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + /* + * This is called when the connection with the service has been + * established, giving us the service object we can use to + * interact with the service. Because we have bound to a explicit + * service that we know is running in our own process, we can + * cast its IBinder to a concrete class and directly access it. + */ + mBoundService = ((ProcessingService.LocalBinder)service).getService(); + mBoundService.setFiltershowActivity(FilterShowActivity.this); + mBoundService.onStart(); + } + + public void onServiceDisconnected(ComponentName className) { + /* + * This is called when the connection with the service has been + * unexpectedly disconnected -- that is, its process crashed. + * Because it is running in our same process, we should never + * see this happen. + */ + mBoundService = null; + } + }; + + void doBindService() { + /* + * Establish a connection with the service. We use an explicit + * class name because we want a specific service implementation that + * we know will be running in our own process (and thus won't be + * supporting component replacement by other applications). + */ + bindService(new Intent(FilterShowActivity.this, ProcessingService.class), + mConnection, Context.BIND_AUTO_CREATE); + mIsBound = true; + } + + void doUnbindService() { + if (mIsBound) { + // Detach our existing connection. + unbindService(mConnection); + mIsBound = false; + } + } + + private void setupPipeline() { + doBindService(); + ImageFilter.setActivityForMemoryToasts(this); + } + + public void updateUIAfterServiceStarted() { + fillCategories(); + loadMainPanel(); + setDefaultPreset(); + extractXMPData(); + processIntent(); + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -160,19 +229,13 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL MasterImage.setMaster(mMasterImage); clearGalleryBitmapPool(); + setupPipeline(); - CachingPipeline.createRenderscriptContext(this); setupMasterImage(); setDefaultValues(); fillEditors(); loadXML(); - loadMainPanel(); - - setDefaultPreset(); - - extractXMPData(); - processIntent(); UsageStatistics.onContentViewChanged(UsageStatistics.COMPONENT_EDITOR, "Main"); UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, UsageStatistics.CATEGORY_LIFECYCLE, UsageStatistics.LIFECYCLE_START); @@ -251,26 +314,25 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL setupEditors(); mEditorPlaceHolder.hide(); - mImageShow.bindAsImageLoadListener(); - fillFx(); - fillBorders(); - fillGeometry(); - fillFilters(); - setupStatePanel(); } + public void fillCategories() { + fillLooks(); + fillBorders(); + fillTools(); + fillEffects(); + } + public void setupStatePanel() { MasterImage.getImage().setHistoryManager(mMasterImage.getHistory()); } - private void fillFilters() { - Vector filtersRepresentations = new Vector(); + private void fillEffects() { FiltersManager filtersManager = FiltersManager.getManager(); - filtersManager.addEffects(filtersRepresentations); - + ArrayList filtersRepresentations = filtersManager.getEffects(); mCategoryFiltersAdapter = new CategoryAdapter(this); for (FilterRepresentation representation : filtersRepresentations) { if (representation.getTextId() != 0) { @@ -280,28 +342,9 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL } } - private void fillGeometry() { - Vector filtersRepresentations = new Vector(); + private void fillTools() { FiltersManager filtersManager = FiltersManager.getManager(); - - GeometryMetadata geo = new GeometryMetadata(); - int[] editorsId = geo.getEditorIds(); - for (int i = 0; i < editorsId.length; i++) { - int editorId = editorsId[i]; - GeometryMetadata geometry = new GeometryMetadata(geo); - geometry.setEditorId(editorId); - EditorInfo editorInfo = (EditorInfo) mEditorPlaceHolder.getEditor(editorId); - geometry.setTextId(editorInfo.getTextId()); - geometry.setOverlayId(editorInfo.getOverlayId()); - geometry.setOverlayOnly(editorInfo.getOverlayOnly()); - if (geometry.getTextId() != 0) { - geometry.setName(getString(geometry.getTextId())); - } - filtersRepresentations.add(geometry); - } - - filtersManager.addTools(filtersRepresentations); - + ArrayList filtersRepresentations = filtersManager.getTools(); mCategoryGeometryAdapter = new CategoryAdapter(this); for (FilterRepresentation representation : filtersRepresentations) { mCategoryGeometryAdapter.add(new Action(this, representation)); @@ -331,7 +374,6 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL mEditorPlaceHolder.setContainer((FrameLayout) findViewById(R.id.editorContainer)); EditorManager.addEditors(mEditorPlaceHolder); mEditorPlaceHolder.setOldViews(mImageViews); - } private void fillEditors() { @@ -347,10 +389,7 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL } private void setDefaultValues() { - ImageFilter.setActivityForMemoryToasts(this); - Resources res = getResources(); - FiltersManager.setResources(res); // TODO: get those values from XML. FramedTextButton.setTextSize((int) getPixelsFromDip(14)); @@ -379,16 +418,11 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL } private void fillBorders() { - Vector borders = new Vector(); - - // The "no border" implementation - borders.add(new FilterImageBorderRepresentation(0)); - - // Google-build borders - FiltersManager.getManager().addBorders(this, borders); + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList borders = filtersManager.getBorders(); for (int i = 0; i < borders.size(); i++) { - FilterRepresentation filter = borders.elementAt(i); + FilterRepresentation filter = borders.get(i); filter.setName(getString(R.string.borders)); if (i == 0) { filter.setName(getString(R.string.none)); @@ -628,16 +662,7 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL if (mLoadBitmapTask != null) { mLoadBitmapTask.cancel(false); } - // TODO: refactor, don't use so many singletons. - FilteringPipeline.getPipeline().turnOnPipeline(false); - MasterImage.reset(); - FilteringPipeline.reset(); - ImageFilter.resetStatics(); - FiltersManager.getPreviewManager().freeRSFilterScripts(); - FiltersManager.getManager().freeRSFilterScripts(); - FiltersManager.getHighresManager().freeRSFilterScripts(); - FiltersManager.reset(); - CachingPipeline.destroyRenderScriptContext(); + doUnbindService(); super.onDestroy(); } @@ -713,7 +738,7 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setType(SharedImageProvider.MIME_TYPE); - mSharedOutputFile = SaveCopyTask.getNewFile(this, MasterImage.getImage().getUri()); + mSharedOutputFile = SaveImage.getNewFile(this, MasterImage.getImage().getUri()); Uri uri = Uri.withAppendedPath(SharedImageProvider.CONTENT_URI, Uri.encode(mSharedOutputFile.getAbsolutePath())); intent.putExtra(Intent.EXTRA_STREAM, uri); @@ -744,7 +769,6 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL @Override public void onPause() { super.onPause(); - rsPause(); if (mShareActionProvider != null) { mShareActionProvider.setOnShareTargetSelectedListener(null); } @@ -753,48 +777,11 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL @Override public void onResume() { super.onResume(); - rsResume(); if (mShareActionProvider != null) { mShareActionProvider.setOnShareTargetSelectedListener(this); } } - private void rsResume() { - ImageFilter.setActivityForMemoryToasts(this); - MasterImage.setMaster(mMasterImage); - if (CachingPipeline.getRenderScriptContext() == null) { - CachingPipeline.createRenderscriptContext(this); - } - FiltersManager.setResources(getResources()); - if (!mLoading) { - Bitmap largeBitmap = MasterImage.getImage().getOriginalBitmapLarge(); - FilteringPipeline pipeline = FilteringPipeline.getPipeline(); - pipeline.setOriginal(largeBitmap); - float previewScale = (float) largeBitmap.getWidth() / - (float) MasterImage.getImage().getOriginalBounds().width(); - pipeline.setPreviewScaleFactor(previewScale); - Bitmap highresBitmap = MasterImage.getImage().getOriginalBitmapHighres(); - if (highresBitmap != null) { - float highResPreviewScale = (float) highresBitmap.getWidth() / - (float) MasterImage.getImage().getOriginalBounds().width(); - pipeline.setHighResPreviewScaleFactor(highResPreviewScale); - } - pipeline.turnOnPipeline(true); - MasterImage.getImage().setOriginalGeometry(largeBitmap); - } - } - - private void rsPause() { - FilteringPipeline.getPipeline().turnOnPipeline(false); - FilteringPipeline.reset(); - ImageFilter.resetStatics(); - FiltersManager.getPreviewManager().freeRSFilterScripts(); - FiltersManager.getManager().freeRSFilterScripts(); - FiltersManager.getHighresManager().freeRSFilterScripts(); - FiltersManager.reset(); - CachingPipeline.destroyRenderScriptContext(); - } - @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -844,16 +831,13 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL } } - private void fillFx() { - FilterFxRepresentation nullFx = - new FilterFxRepresentation(getString(R.string.none), 0, R.string.none); - Vector filtersRepresentations = new Vector(); - FiltersManager.getManager().addLooks(this, filtersRepresentations); + private void fillLooks() { + FiltersManager filtersManager = FiltersManager.getManager(); + ArrayList filtersRepresentations = filtersManager.getLooks(); mCategoryLooksAdapter = new CategoryAdapter(this); int verticalItemHeight = (int) getResources().getDimension(R.dimen.action_item_height); mCategoryLooksAdapter.setItemHeight(verticalItemHeight); - mCategoryLooksAdapter.add(new Action(this, nullFx, Action.FULL_VIEW)); for (FilterRepresentation representation : filtersRepresentations) { mCategoryLooksAdapter.add(new Action(this, representation, Action.FULL_VIEW)); } @@ -1030,7 +1014,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, mSelectedImageUri); + File saveDir = SaveImage.getFinalSaveDirectory(this, mSelectedImageUri); int bucketId = GalleryUtils.getBucketId(saveDir.getPath()); String albumName = LocalAlbum.getLocalizedName(getResources(), bucketId, null); showSavingProgress(albumName); @@ -1063,9 +1047,4 @@ public class FilterShowActivity extends FragmentActivity implements OnItemClickL 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 7c594c67f..b6c72fd9d 100644 --- a/src/com/android/gallery3d/filtershow/cache/ImageLoader.java +++ b/src/com/android/gallery3d/filtershow/cache/ImageLoader.java @@ -18,6 +18,7 @@ package com.android.gallery3d.filtershow.cache; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteException; @@ -33,6 +34,7 @@ import android.webkit.MimeTypeMap; import com.adobe.xmp.XMPException; import com.adobe.xmp.XMPMeta; +import com.android.gallery3d.R; import com.android.gallery3d.common.Utils; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.filtershow.imageshow.MasterImage; @@ -286,17 +288,20 @@ public final class ImageLoader { * @param uri URI of image to open. * @param context context whose ContentResolver to use. * @param maxSideLength max side length of returned bitmap. - * @param originalBounds set to the actual bounds of the stored bitmap. + * @param originalBounds If not null, set to the actual bounds of the stored bitmap. + * @param useMin use min or max side of the original image * @return downsampled bitmap or null if this operation failed. */ public static Bitmap loadConstrainedBitmap(Uri uri, Context context, int maxSideLength, - Rect originalBounds) { - if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) { + Rect originalBounds, boolean useMin) { + if (maxSideLength <= 0 || uri == null || context == null) { throw new IllegalArgumentException("bad argument to getScaledBitmap"); } // Get width and height of stored bitmap Rect storedBounds = loadBitmapBounds(context, uri); - originalBounds.set(storedBounds); + if (originalBounds != null) { + originalBounds.set(storedBounds); + } int w = storedBounds.width(); int h = storedBounds.height(); @@ -306,7 +311,12 @@ public final class ImageLoader { } // Find best downsampling size - int imageSide = Math.max(w, h); + int imageSide = 0; + if (useMin) { + imageSide = Math.min(w, h); + } else { + imageSide = Math.max(w, h); + } int sampleSize = 1; while (imageSide > maxSideLength) { imageSide >>>= 1; @@ -336,7 +346,7 @@ public final class ImageLoader { */ public static Bitmap loadOrientedConstrainedBitmap(Uri uri, Context context, int maxSideLength, int orientation, Rect originalBounds) { - Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds); + Bitmap bmap = loadConstrainedBitmap(uri, context, maxSideLength, originalBounds, false); if (bmap != null) { bmap = orientBitmap(bmap, orientation); } diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java index d14c090fa..0a0c36703 100644 --- a/src/com/android/gallery3d/filtershow/crop/CropActivity.java +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -45,7 +45,7 @@ import android.widget.Toast; import com.android.gallery3d.R; import com.android.gallery3d.common.Utils; import com.android.gallery3d.filtershow.cache.ImageLoader; -import com.android.gallery3d.filtershow.tools.SaveCopyTask; +import com.android.gallery3d.filtershow.tools.SaveImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -264,7 +264,7 @@ public class CropActivity extends Activity { protected Bitmap doInBackground(Uri... params) { Uri uri = params[0]; Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize, - mOriginalBounds); + mOriginalBounds, false); mOrientation = ImageLoader.getMetadataRotation(mContext, uri); return bmap; } @@ -299,7 +299,7 @@ public class CropActivity extends Activity { } } if (flags == 0) { - destinationUri = SaveCopyTask.makeAndInsertUri(this, mSourceUri); + destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri); if (destinationUri != null) { flags |= DO_EXTRA_OUTPUT; } diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java index c0a6c139e..1d3a10d7a 100644 --- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java +++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java @@ -20,8 +20,10 @@ import android.content.res.Resources; import android.util.Log; import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.imageshow.GeometryMetadata; import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import java.util.ArrayList; import java.util.HashMap; import java.util.Vector; @@ -30,6 +32,11 @@ public abstract class BaseFiltersManager implements FiltersManagerInterface { protected HashMap mRepresentationLookup = null; private static final String LOGTAG = "BaseFiltersManager"; + protected ArrayList mLooks = new ArrayList(); + protected ArrayList mBorders = new ArrayList(); + protected ArrayList mTools = new ArrayList(); + protected ArrayList mEffects = new ArrayList(); + protected void init() { mFilters = new HashMap(); mRepresentationLookup = new HashMap(); @@ -133,11 +140,27 @@ public abstract class BaseFiltersManager implements FiltersManagerInterface { filters.add(ImageFilterGeometry.class); } - public void addBorders(Context context, Vector representations) { + public ArrayList getLooks() { + return mLooks; + } + + public ArrayList getBorders() { + return mBorders; + } + + public ArrayList getTools() { + return mTools; + } + public ArrayList getEffects() { + return mEffects; } - public void addLooks(Context context, Vector representations) { + public void addBorders(Context context) { + + } + + public void addLooks(Context context) { int[] drawid = { R.drawable.filtershow_fx_0005_punch, R.drawable.filtershow_fx_0000_vintage, @@ -175,37 +198,72 @@ public abstract class BaseFiltersManager implements FiltersManagerInterface { "LUT3D_XPROCESS" }; + FilterFxRepresentation nullFx = + new FilterFxRepresentation(context.getString(R.string.none), + 0, R.string.none); + mLooks.add(nullFx); + for (int i = 0; i < drawid.length; i++) { FilterFxRepresentation fx = new FilterFxRepresentation( context.getString(fxNameid[i]), drawid[i], fxNameid[i]); fx.setSerializationName(serializationNames[i]); - representations.add(fx); + mLooks.add(fx); addRepresentation(fx); } } - public void addEffects(Vector representations) { - representations.add(getRepresentation(ImageFilterTinyPlanet.class)); - representations.add(getRepresentation(ImageFilterWBalance.class)); - representations.add(getRepresentation(ImageFilterExposure.class)); - representations.add(getRepresentation(ImageFilterVignette.class)); - representations.add(getRepresentation(ImageFilterContrast.class)); - representations.add(getRepresentation(ImageFilterShadows.class)); - representations.add(getRepresentation(ImageFilterHighlights.class)); - representations.add(getRepresentation(ImageFilterVibrance.class)); - representations.add(getRepresentation(ImageFilterSharpen.class)); - representations.add(getRepresentation(ImageFilterCurves.class)); - representations.add(getRepresentation(ImageFilterHue.class)); - representations.add(getRepresentation(ImageFilterSaturated.class)); - representations.add(getRepresentation(ImageFilterBwFilter.class)); - representations.add(getRepresentation(ImageFilterNegative.class)); - representations.add(getRepresentation(ImageFilterEdge.class)); - representations.add(getRepresentation(ImageFilterKMeans.class)); - } - - public void addTools(Vector representations) { - representations.add(getRepresentation(ImageFilterRedEye.class)); - representations.add(getRepresentation(ImageFilterDraw.class)); + public void addEffects() { + mEffects.add(getRepresentation(ImageFilterTinyPlanet.class)); + mEffects.add(getRepresentation(ImageFilterWBalance.class)); + mEffects.add(getRepresentation(ImageFilterExposure.class)); + mEffects.add(getRepresentation(ImageFilterVignette.class)); + mEffects.add(getRepresentation(ImageFilterContrast.class)); + mEffects.add(getRepresentation(ImageFilterShadows.class)); + mEffects.add(getRepresentation(ImageFilterHighlights.class)); + mEffects.add(getRepresentation(ImageFilterVibrance.class)); + mEffects.add(getRepresentation(ImageFilterSharpen.class)); + mEffects.add(getRepresentation(ImageFilterCurves.class)); + mEffects.add(getRepresentation(ImageFilterHue.class)); + mEffects.add(getRepresentation(ImageFilterSaturated.class)); + mEffects.add(getRepresentation(ImageFilterBwFilter.class)); + mEffects.add(getRepresentation(ImageFilterNegative.class)); + mEffects.add(getRepresentation(ImageFilterEdge.class)); + mEffects.add(getRepresentation(ImageFilterKMeans.class)); + } + + public void addTools(Context context) { + GeometryMetadata geo = new GeometryMetadata(); + int[] editorsId = geo.getEditorIds(); + + int[] textId = { + R.string.crop, + R.string.straighten, + R.string.rotate, + R.string.mirror + }; + + int[] overlayId = { + R.drawable.filtershow_button_geometry_crop, + R.drawable.filtershow_button_geometry_straighten, + R.drawable.filtershow_button_geometry_rotate, + R.drawable.filtershow_button_geometry_flip + }; + + for (int i = 0; i < editorsId.length; i++) { + int editorId = editorsId[i]; + GeometryMetadata geometry = new GeometryMetadata(geo); + geometry.setEditorId(editorId); + geometry.setTextId(textId[i]); + geometry.setOverlayId(overlayId[i]); + geometry.setOverlayOnly(true); + if (geometry.getTextId() != 0) { + geometry.setName(context.getString(geometry.getTextId())); + } + mTools.add(geometry); + } + + mTools.add(getRepresentation(ImageFilterRedEye.class)); + mTools.add(getRepresentation(ImageFilterDraw.class)); } public void setFilterResources(Resources resources) { diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java index 2da358c2c..1a304db69 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageShow.java @@ -40,7 +40,7 @@ import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.filters.ImageFilter; import com.android.gallery3d.filtershow.pipeline.ImagePreset; -import com.android.gallery3d.filtershow.tools.SaveCopyTask; +import com.android.gallery3d.filtershow.tools.SaveImage; import java.io.File; @@ -395,7 +395,7 @@ public class ImageShow extends View implements OnGestureListener, } public void saveImage(FilterShowActivity filterShowActivity, File file) { - SaveCopyTask.saveImage(getImagePreset(), filterShowActivity, file); + SaveImage.saveImage(getImagePreset(), filterShowActivity, file); } diff --git a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java index ed09fb116..01fe3c159 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java +++ b/src/com/android/gallery3d/filtershow/imageshow/MasterImage.java @@ -482,6 +482,9 @@ public class MasterImage implements RenderingRequestCaller { } public void needsUpdatePartialPreview() { + if (mPreset == null) { + return; + } if (!mPreset.canDoPartialRendering()) { invalidatePartialPreview(); return; diff --git a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java index a7580a835..535d02f0b 100644 --- a/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java +++ b/src/com/android/gallery3d/filtershow/pipeline/CachingPipeline.java @@ -16,7 +16,7 @@ package com.android.gallery3d.filtershow.pipeline; -import android.app.Activity; +import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.support.v8.renderscript.Allocation; @@ -68,7 +68,7 @@ public class CachingPipeline implements PipelineInterface { return sRS; } - public static synchronized void createRenderscriptContext(Activity context) { + public static synchronized void createRenderscriptContext(Context context) { if (sRS != null) { Log.w(LOGTAG, "A prior RS context exists when calling setRenderScriptContext"); destroyRenderScriptContext(); diff --git a/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java b/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java index a302b1907..0e9b83d7f 100644 --- a/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java +++ b/src/com/android/gallery3d/filtershow/pipeline/FilteringPipeline.java @@ -21,7 +21,6 @@ import android.os.*; import android.os.Process; import android.util.Log; -import com.android.gallery3d.filtershow.cache.ImageLoader; import com.android.gallery3d.filtershow.filters.FiltersManager; import com.android.gallery3d.filtershow.imageshow.MasterImage; diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java index 78a4d211d..2b9e3701f 100644 --- a/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java +++ b/src/com/android/gallery3d/filtershow/pipeline/ImagePreset.java @@ -55,7 +55,7 @@ public class ImagePreset { private boolean mPartialRendering = false; private Rect mPartialRenderingBounds; - private static final boolean DEBUG = true; + private static final boolean DEBUG = false; public ImagePreset() { } @@ -607,7 +607,7 @@ public class ImagePreset { if (DEBUG) { Log.v(LOGTAG, "Serialization: " + sname); if (sname == null) { - Log.v(LOGTAG, "Serialization: " + filter); + Log.v(LOGTAG, "Serialization name null for filter: " + filter); } } writer.name(sname); diff --git a/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java new file mode 100644 index 000000000..e93ec1687 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ImageSavingTask.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2013 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.pipeline; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ImageSavingTask extends ProcessingTask { + private ProcessingService mProcessingService; + + static class SaveRequest implements Request { + Uri sourceUri; + Uri selectedUri; + File destinationFile; + ImagePreset preset; + } + + static class UpdateBitmap implements Update { + Bitmap bitmap; + } + + static class UpdateProgress implements Update { + int max; + int current; + } + + static class URIResult implements Result { + Uri uri; + } + + public ImageSavingTask(ProcessingService service) { + mProcessingService = service; + } + + public void saveImage(Uri sourceUri, Uri selectedUri, + File destinationFile, ImagePreset preset) { + SaveRequest request = new SaveRequest(); + request.sourceUri = sourceUri; + request.selectedUri = selectedUri; + request.destinationFile = destinationFile; + request.preset = preset; + postRequest(request); + } + + public Result doInBackground(Request message) { + SaveRequest request = (SaveRequest) message; + Uri sourceUri = request.sourceUri; + Uri selectedUri = request.selectedUri; + File destinationFile = request.destinationFile; + ImagePreset preset = request.preset; + + // We create a small bitmap showing the result that we can + // give to the notification + UpdateBitmap updateBitmap = new UpdateBitmap(); + updateBitmap.bitmap = createNotificationBitmap(sourceUri, preset); + postUpdate(updateBitmap); + + SaveImage saveImage = new SaveImage(mProcessingService, sourceUri, + selectedUri, destinationFile, + new SaveImage.Callback() { + @Override + public void onProgress(int max, int current) { + UpdateProgress updateProgress = new UpdateProgress(); + updateProgress.max = max; + updateProgress.current = current; + postUpdate(updateProgress); + } + }); + + Uri uri = saveImage.processAndSaveImage(preset); + URIResult result = new URIResult(); + result.uri = uri; + return result; + } + + @Override + public void onResult(Result message) { + URIResult result = (URIResult) message; + mProcessingService.completeSaveImage(result.uri); + } + + @Override + public void onUpdate(Update message) { + if (message instanceof UpdateBitmap) { + Bitmap bitmap = ((UpdateBitmap) message).bitmap; + mProcessingService.updateNotificationWithBitmap(bitmap); + } + if (message instanceof UpdateProgress) { + UpdateProgress progress = (UpdateProgress) message; + mProcessingService.updateProgress(progress.max, progress.current); + } + } + + private Bitmap createNotificationBitmap(Uri sourceUri, ImagePreset preset) { + int notificationBitmapSize = Resources.getSystem().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + Bitmap bitmap = ImageLoader.loadConstrainedBitmap(sourceUri, getContext(), + notificationBitmapSize, null, true); + CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), "Thumb"); + return pipeline.renderFinalImage(bitmap, preset); + } + +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java new file mode 100644 index 000000000..032024746 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingService.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2013 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.pipeline; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; +import com.android.gallery3d.R; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.filters.ImageFilter; +import com.android.gallery3d.filtershow.tools.SaveImage; + +import java.io.File; + +public class ProcessingService extends Service { + private static final String LOGTAG = "ProcessingService"; + private static final boolean SHOW_IMAGE = false; + private int mNotificationId; + private NotificationManager mNotifyMgr = null; + private Notification.Builder mBuilder = null; + + private static final String PRESET = "preset"; + private static final String SOURCE_URI = "sourceUri"; + private static final String SELECTED_URI = "selectedUri"; + private static final String DESTINATION_FILE = "destinationFile"; + private static final String SAVING = "saving"; + + private ProcessingTaskController mProcessingTaskController; + private ImageSavingTask mImageSavingTask; + + private final IBinder mBinder = new LocalBinder(); + private FilterShowActivity mFiltershowActivity; + + private boolean mSaving = false; + private boolean mNeedsAlive = false; + + public void setFiltershowActivity(FilterShowActivity filtershowActivity) { + mFiltershowActivity = filtershowActivity; + } + + public class LocalBinder extends Binder { + public ProcessingService getService() { + return ProcessingService.this; + } + } + + public static Intent getSaveIntent(Context context, ImagePreset preset, File destination, + Uri selectedImageUri, Uri sourceImageUri) { + Intent processIntent = new Intent(context, ProcessingService.class); + processIntent.putExtra(ProcessingService.SOURCE_URI, + sourceImageUri.toString()); + processIntent.putExtra(ProcessingService.SELECTED_URI, + selectedImageUri.toString()); + if (destination != null) { + processIntent.putExtra(ProcessingService.DESTINATION_FILE, destination.toString()); + } + processIntent.putExtra(ProcessingService.PRESET, + preset.getJsonString(context.getString(R.string.saved))); + processIntent.putExtra(ProcessingService.SAVING, true); + return processIntent; + } + + + @Override + public void onCreate() { + mProcessingTaskController = new ProcessingTaskController(this); + mImageSavingTask = new ImageSavingTask(this); + mProcessingTaskController.add(mImageSavingTask); + setupPipeline(); + } + + @Override + public void onDestroy() { + tearDownPipeline(); + mProcessingTaskController.quit(); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + mNeedsAlive = true; + if (intent != null && intent.getBooleanExtra(SAVING, false)) { + // we save using an intent to keep the service around after the + // activity has been destroyed. + String presetJson = intent.getStringExtra(PRESET); + String source = intent.getStringExtra(SOURCE_URI); + String selected = intent.getStringExtra(SELECTED_URI); + String destination = intent.getStringExtra(DESTINATION_FILE); + Uri sourceUri = Uri.parse(source); + Uri selectedUri = null; + if (selected != null) { + selectedUri = Uri.parse(selected); + } + File destinationFile = null; + if (destination != null) { + destinationFile = new File(destination); + } + ImagePreset preset = new ImagePreset(); + preset.readJsonFromString(presetJson); + mNeedsAlive = false; + mSaving = true; + handleSaveRequest(sourceUri, selectedUri, destinationFile, preset); + } + return START_REDELIVER_INTENT; + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + public void onStart() { + mNeedsAlive = true; + if (!mSaving && mFiltershowActivity != null) { + mFiltershowActivity.updateUIAfterServiceStarted(); + } + } + + public void handleSaveRequest(Uri sourceUri, Uri selectedUri, + File destinationFile, ImagePreset preset) { + mNotifyMgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotificationId++; + + mBuilder = + new Notification.Builder(this) + .setSmallIcon(R.drawable.filtershow_button_fx) + .setContentTitle(getString(R.string.filtershow_notification_label)) + .setContentText(getString(R.string.filtershow_notification_message)); + + startForeground(mNotificationId, mBuilder.build()); + + updateProgress(SaveImage.MAX_PROCESSING_STEPS, 0); + + // Process the image + + mImageSavingTask.saveImage(sourceUri, selectedUri, destinationFile, preset); + } + + public void updateNotificationWithBitmap(Bitmap bitmap) { + mBuilder.setLargeIcon(bitmap); + mNotifyMgr.notify(mNotificationId, mBuilder.build()); + } + + public void updateProgress(int max, int current) { + mBuilder.setProgress(max, current, false); + mNotifyMgr.notify(mNotificationId, mBuilder.build()); + } + + public void completeSaveImage(Uri result) { + if (SHOW_IMAGE) { + // TODO: we should update the existing image in Gallery instead + Intent viewImage = new Intent(Intent.ACTION_VIEW, result); + viewImage.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(viewImage); + } + stopForeground(true); + stopSelf(); + if (mNeedsAlive) { + // If the app has been restarted while we were saving... + mFiltershowActivity.updateUIAfterServiceStarted(); + } else if (mFiltershowActivity.isSimpleEditAction()) { + // terminate now + mFiltershowActivity.completeSaveImage(result); + } + } + + private void setupPipeline() { + Resources res = getResources(); + FiltersManager.setResources(res); + CachingPipeline.createRenderscriptContext(this); + + FiltersManager filtersManager = FiltersManager.getManager(); + filtersManager.addLooks(this); + filtersManager.addBorders(this); + filtersManager.addTools(this); + filtersManager.addEffects(); + } + + private void tearDownPipeline() { + FilteringPipeline.getPipeline().turnOnPipeline(false); + FilteringPipeline.reset(); + ImageFilter.resetStatics(); + FiltersManager.getPreviewManager().freeRSFilterScripts(); + FiltersManager.getManager().freeRSFilterScripts(); + FiltersManager.getHighresManager().freeRSFilterScripts(); + FiltersManager.reset(); + CachingPipeline.destroyRenderScriptContext(); + } + + static { + System.loadLibrary("jni_filtershow_filters"); + } +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java new file mode 100644 index 000000000..c3687ee6a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTask.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2013 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.pipeline; + +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +public abstract class ProcessingTask { + private ProcessingTaskController mTaskController; + private Handler mProcessingHandler; + private Handler mResultHandler; + private int mType; + + static interface Request {} + static interface Update {} + static interface Result {} + + public void postRequest(Request message) { + Message msg = mProcessingHandler.obtainMessage(mType); + msg.obj = message; + mProcessingHandler.sendMessage(msg); + } + + public void postUpdate(Update message) { + Message msg = mResultHandler.obtainMessage(mType); + msg.obj = message; + msg.arg1 = ProcessingTaskController.UPDATE; + mResultHandler.sendMessage(msg); + } + + public void processRequest(Request message) { + Object result = doInBackground(message); + Message msg = mResultHandler.obtainMessage(mType); + msg.obj = result; + msg.arg1 = ProcessingTaskController.RESULT; + mResultHandler.sendMessage(msg); + } + + public void added(ProcessingTaskController taskController) { + mTaskController = taskController; + mResultHandler = taskController.getResultHandler(); + mProcessingHandler = taskController.getProcessingHandler(); + mType = taskController.getReservedType(); + } + + public int getType() { + return mType; + } + + public Context getContext() { + return mTaskController.getContext(); + } + + public abstract Result doInBackground(Request message); + public abstract void onResult(Result message); + public void onUpdate(Update message) {} +} diff --git a/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java new file mode 100644 index 000000000..218ea630a --- /dev/null +++ b/src/com/android/gallery3d/filtershow/pipeline/ProcessingTaskController.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2013 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.pipeline; + +import android.content.Context; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; + +import java.util.HashMap; + +public class ProcessingTaskController implements Handler.Callback { + private static final String LOGTAG = "ProcessingTaskController"; + + private Context mContext; + private HandlerThread mHandlerThread = null; + private Handler mProcessingHandler = null; + private int mCurrentType; + private HashMap mTasks = new HashMap(); + + public final static int RESULT = 1; + public final static int UPDATE = 2; + + private final Handler mResultHandler = new Handler() { + + @Override + public void handleMessage(Message msg) { + ProcessingTask task = mTasks.get(msg.what); + if (task != null) { + if (msg.arg1 == RESULT) { + task.onResult((ProcessingTask.Result) msg.obj); + } else if (msg.arg1 == UPDATE) { + task.onUpdate((ProcessingTask.Update) msg.obj); + } else { + Log.w(LOGTAG, "received unknown message! " + msg.arg1); + } + } + } + }; + + @Override + public boolean handleMessage(Message msg) { + ProcessingTask task = mTasks.get(msg.what); + if (task != null) { + task.processRequest((ProcessingTask.Request) msg.obj); + return true; + } + return false; + } + + public ProcessingTaskController(Context context) { + mContext = context; + mHandlerThread = new HandlerThread("ProcessingTaskController", + android.os.Process.THREAD_PRIORITY_FOREGROUND); + mHandlerThread.start(); + mProcessingHandler = new Handler(mHandlerThread.getLooper(), this); + } + + public Handler getProcessingHandler() { + return mProcessingHandler; + } + + public Handler getResultHandler() { + return mResultHandler; + } + + public int getReservedType() { + return mCurrentType++; + } + + public Context getContext() { + return mContext; + } + + public void add(ProcessingTask task) { + task.added(this); + mTasks.put(task.getType(), task); + } + + public void quit() { + mHandlerThread.quit(); + } +} diff --git a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java b/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java deleted file mode 100644 index dcf0ae166..000000000 --- a/src/com/android/gallery3d/filtershow/tools/SaveCopyTask.java +++ /dev/null @@ -1,611 +0,0 @@ -/* - * 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.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.provider.MediaStore.Images.Media; -import android.util.Log; - -import com.android.gallery3d.common.Utils; -import com.android.gallery3d.exif.ExifInterface; -import com.android.gallery3d.filtershow.FilterShowActivity; -import com.android.gallery3d.filtershow.pipeline.CachingPipeline; -import com.android.gallery3d.filtershow.cache.ImageLoader; -import com.android.gallery3d.filtershow.filters.FiltersManager; -import com.android.gallery3d.filtershow.imageshow.MasterImage; -import com.android.gallery3d.filtershow.pipeline.ImagePreset; -import com.android.gallery3d.util.UsageStatistics; -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; -import java.text.SimpleDateFormat; -import java.util.TimeZone; - -/** - * Asynchronous task for saving edited photo as a new copy. - */ -public class SaveCopyTask extends AsyncTask { - - private static final String LOGTAG = "SaveCopyTask"; - - /** - * Callback for the completed asynchronous task. - */ - public interface Callback { - void onComplete(Uri result); - } - - public interface ContentResolverQueryCallback { - void onCursorResult(Cursor cursor); - } - - private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss"; - 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"; - - // When this is true, the source file will be saved into auxiliary directory - // and hidden from MediaStore. Otherwise, the source will be kept as the - // same. - private static final boolean USE_AUX_DIR = true; - - private final Context mContext; - private final Uri mSourceUri; - private final Callback mCallback; - private final File mDestinationFile; - private final Uri mSelectedImageUri; - public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; - - // 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. - - /** - * @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) { - mDestinationFile = getNewFile(context, selectedImageUri); - } else { - mDestinationFile = destination; - } - - mSelectedImageUri = selectedImageUri; - } - - public static File getFinalSaveDirectory(Context context, Uri sourceUri) { - File saveDirectory = SaveCopyTask.getSaveDirectory(context, sourceUri); - if ((saveDirectory == null) || !saveDirectory.canWrite()) { - saveDirectory = new File(Environment.getExternalStorageDirectory(), - SaveCopyTask.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())); - if (hasPanoPrefix(context, sourceUri)) { - return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG); - } - 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 = mContext.getContentResolver().openInputStream(source); - xmp = XmpUtilHelper.extractXMPMeta(is); - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "Failed to get XMP data from image: ", e); - } finally { - Utils.closeSilently(is); - } - } - return xmp; - } - - public boolean putPanoramaXMPData(File file, Object xmp) { - if (xmp != null) { - return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp); - } - return false; - } - - public ExifInterface getExifData(Uri source) { - ExifInterface exif = new ExifInterface(); - String mimeType = mContext.getContentResolver().getType(mSelectedImageUri); - if (mimeType == null) { - mimeType = ImageLoader.getMimeType(mSelectedImageUri); - } - if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) { - InputStream inStream = null; - try { - inStream = mContext.getContentResolver().openInputStream(source); - exif.readExif(inStream); - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "Cannot find file: " + source, e); - } catch (IOException e) { - Log.w(LOGTAG, "Cannot read exif for: " + source, e); - } finally { - Utils.closeSilently(inStream); - } - } - return exif; - } - - public boolean putExifData(File file, ExifInterface exif, Bitmap image) { - boolean ret = false; - try { - exif.writeExif(image, file.getAbsolutePath()); - ret = true; - } catch (FileNotFoundException e) { - Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e); - } catch (IOException e) { - Log.w(LOGTAG, "Could not write exif: ", e); - } - return ret; - } - - /** - * 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 || mSourceUri == null || mSelectedImageUri == null) { - return null; - } - - ImagePreset preset = params[0]; - Uri uri = null; - if (!preset.hasModifications()) { - // This can happen only when preset has no modification but save - // button is enabled, it means the file is loaded with filters in - // the XMP, then all the filters are removed or restore to default. - // In this case, when mSourceUri exists, rename it to the - // destination file. - File srcFile = getLocalFileFromUri(mContext, mSourceUri); - // If the source is not a local file, then skip this renaming and - // create a local copy as usual. - if (srcFile != null) { - srcFile.renameTo(mDestinationFile); - uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile, - System.currentTimeMillis()); - removeSelectedImage(); - return uri; - } - } - - boolean noBitmap = true; - int num_tries = 0; - int sampleSize = 1; - - // 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; - if (USE_AUX_DIR) { - newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile); - } else { - newSourceUri = mSourceUri; - } - // Stopgap fix for low-memory devices. - while (noBitmap) { - try { - // Try to do bitmap operations, downsample if low-memory - Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri, - sampleSize); - if (bitmap == null) { - return null; - } - CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), - "Saving"); - bitmap = pipeline.renderFinalImage(bitmap, preset); - - Object xmp = getPanoramaXMPData(mSelectedImageUri, preset); - ExifInterface exif = getExifData(mSelectedImageUri); - - // Set tags - long time = System.currentTimeMillis(); - exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time, - TimeZone.getDefault()); - exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, - ExifInterface.Orientation.TOP_LEFT)); - - // Remove old thumbnail - exif.removeCompressedThumbnail(); - - // If we succeed in writing the bitmap as a jpeg, return a uri. - if (putExifData(mDestinationFile, exif, bitmap)) { - putPanoramaXMPData(mDestinationFile, xmp); - uri = SaveCopyTask.insertContent(mContext, mSelectedImageUri, mDestinationFile, - time); - } - - // 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. - if (USE_AUX_DIR) { - removeSelectedImage(); - } - noBitmap = false; - UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, - "SaveComplete", null); - } catch (java.lang.OutOfMemoryError e) { - // Try 5 times before failing for good. - if (++num_tries >= 5) { - throw e; - } - System.gc(); - sampleSize *= 2; - } - } - return uri; - } - - private void removeSelectedImage() { - String scheme = mSelectedImageUri.getScheme(); - if (scheme != null && scheme.equals(ContentResolver.SCHEME_CONTENT)) { - if (mSelectedImageUri.getAuthority().equals(MediaStore.AUTHORITY)) { - mContext.getContentResolver().delete(mSelectedImageUri, null, null); - } - } - } - - /** - * 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 = getLocalFileFromUri(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 (mCallback != null) { - mCallback.onComplete(result); - } - } - - public static Uri makeAndInsertUri(Context context, Uri sourceUri) { - long time = System.currentTimeMillis(); - String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); - File saveDirectory = getFinalSaveDirectory(context, sourceUri); - File file = new File(saveDirectory, filename + ".JPG"); - return insertContent(context, sourceUri, file, time); - } - - public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, - File destination) { - Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); - new SaveCopyTask(filterShowActivity, MasterImage.getImage().getUri(), selectedImageUri, - destination, - new SaveCopyTask.Callback() { - - @Override - public void onComplete(Uri result) { - filterShowActivity.completeSaveImage(result); - } - - }).execute(preset); - } - - - public 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, - 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) { - File file = getLocalFileFromUri(context, sourceUri); - if (file != null) { - return file.getParentFile(); - } else { - return null; - } - } - - /** - * 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 getLocalFileFromUri(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]; - } - - /** - * Gets the actual filename for a Uri from Gallery's ContentProvider. - */ - private static String getTrueFilename(Context context, Uri src) { - if (context == null || src == null) { - return null; - } - final String[] trueName = new String[1]; - querySource(context, src, new String[] { - ImageColumns.DATA - }, new ContentResolverQueryCallback() { - @Override - public void onCursorResult(Cursor cursor) { - trueName[0] = new File(cursor.getString(0)).getName(); - } - }); - return trueName[0]; - } - - /** - * Checks whether the true filename has the panorama image prefix. - */ - private static boolean hasPanoPrefix(Context context, Uri src) { - String name = getTrueFilename(context, src); - return name != null && name.startsWith(PREFIX_PANO); - } - - /** - * Insert the content (saved file) with proper source photo properties. - */ - public static Uri insertContent(Context context, Uri sourceUri, File file, long time) { - time /= 1000; - - final ContentValues values = new ContentValues(); - 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_ADDED, time); - 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, - }; - SaveCopyTask.querySource(context, sourceUri, projection, - new SaveCopyTask.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); - } - -} diff --git a/src/com/android/gallery3d/filtershow/tools/SaveImage.java b/src/com/android/gallery3d/filtershow/tools/SaveImage.java new file mode 100644 index 000000000..9b13af13d --- /dev/null +++ b/src/com/android/gallery3d/filtershow/tools/SaveImage.java @@ -0,0 +1,628 @@ +/* + * 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.content.Intent; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; + +import com.android.gallery3d.R; +import com.android.gallery3d.app.PhotoPage; +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; +import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.pipeline.CachingPipeline; +import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.filters.FiltersManager; +import com.android.gallery3d.filtershow.imageshow.MasterImage; +import com.android.gallery3d.filtershow.pipeline.ImagePreset; +import com.android.gallery3d.filtershow.pipeline.ProcessingService; +import com.android.gallery3d.util.UsageStatistics; +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; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * Handles saving edited photo + */ +public class SaveImage { + private static final String LOGTAG = "SaveImage"; + + /** + * Callback for updates + */ + public interface Callback { + void onProgress(int max, int current); + } + + public interface ContentResolverQueryCallback { + void onCursorResult(Cursor cursor); + } + + private static final String TIME_STAMP_NAME = "_yyyyMMdd_HHmmss"; + 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"; + + // When this is true, the source file will be saved into auxiliary directory + // and hidden from MediaStore. Otherwise, the source will be kept as the + // same. + private static final boolean USE_AUX_DIR = true; + + private final Context mContext; + private final Uri mSourceUri; + private final Callback mCallback; + private final File mDestinationFile; + private final Uri mSelectedImageUri; + + private int mCurrentProcessingStep = 1; + + public static final int MAX_PROCESSING_STEPS = 6; + public static final String DEFAULT_SAVE_DIRECTORY = "EditedOnlinePhotos"; + + // 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. + + /** + * @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 SaveImage(Context context, Uri sourceUri, Uri selectedImageUri, + File destination, Callback callback) { + mContext = context; + mSourceUri = sourceUri; + mCallback = callback; + if (destination == null) { + mDestinationFile = getNewFile(context, selectedImageUri); + } else { + mDestinationFile = destination; + } + + mSelectedImageUri = selectedImageUri; + } + + public static File getFinalSaveDirectory(Context context, Uri sourceUri) { + File saveDirectory = SaveImage.getSaveDirectory(context, sourceUri); + if ((saveDirectory == null) || !saveDirectory.canWrite()) { + saveDirectory = new File(Environment.getExternalStorageDirectory(), + SaveImage.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())); + if (hasPanoPrefix(context, sourceUri)) { + return new File(saveDirectory, PREFIX_PANO + filename + POSTFIX_JPG); + } + 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 = mContext.getContentResolver().openInputStream(source); + xmp = XmpUtilHelper.extractXMPMeta(is); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Failed to get XMP data from image: ", e); + } finally { + Utils.closeSilently(is); + } + } + return xmp; + } + + public boolean putPanoramaXMPData(File file, Object xmp) { + if (xmp != null) { + return XmpUtilHelper.writeXMPMeta(file.getAbsolutePath(), xmp); + } + return false; + } + + public ExifInterface getExifData(Uri source) { + ExifInterface exif = new ExifInterface(); + String mimeType = mContext.getContentResolver().getType(mSelectedImageUri); + if (mimeType == null) { + mimeType = ImageLoader.getMimeType(mSelectedImageUri); + } + if (mimeType.equals(ImageLoader.JPEG_MIME_TYPE)) { + InputStream inStream = null; + try { + inStream = mContext.getContentResolver().openInputStream(source); + exif.readExif(inStream); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "Cannot find file: " + source, e); + } catch (IOException e) { + Log.w(LOGTAG, "Cannot read exif for: " + source, e); + } finally { + Utils.closeSilently(inStream); + } + } + return exif; + } + + public boolean putExifData(File file, ExifInterface exif, Bitmap image) { + boolean ret = false; + try { + exif.writeExif(image, file.getAbsolutePath()); + ret = true; + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "File not found: " + file.getAbsolutePath(), e); + } catch (IOException e) { + Log.w(LOGTAG, "Could not write exif: ", e); + } + return ret; + } + + private Uri resetToOriginalImageIfNeeded(ImagePreset preset) { + Uri uri = null; + if (!preset.hasModifications()) { + // This can happen only when preset has no modification but save + // button is enabled, it means the file is loaded with filters in + // the XMP, then all the filters are removed or restore to default. + // In this case, when mSourceUri exists, rename it to the + // destination file. + File srcFile = getLocalFileFromUri(mContext, mSourceUri); + // If the source is not a local file, then skip this renaming and + // create a local copy as usual. + if (srcFile != null) { + srcFile.renameTo(mDestinationFile); + uri = SaveImage.insertContent(mContext, mSelectedImageUri, mDestinationFile, + System.currentTimeMillis()); + removeSelectedImage(); + } + } + return uri; + } + + private void resetProgress() { + mCurrentProcessingStep = 0; + } + + private void updateProgress() { + if (mCallback != null) { + mCallback.onProgress(MAX_PROCESSING_STEPS, ++mCurrentProcessingStep); + } + } + + public Uri processAndSaveImage(ImagePreset preset) { + + Uri uri = resetToOriginalImageIfNeeded(preset); + if (uri != null) { + return null; + } + + resetProgress(); + + boolean noBitmap = true; + int num_tries = 0; + int sampleSize = 1; + + // 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; + if (USE_AUX_DIR) { + newSourceUri = moveSrcToAuxIfNeeded(mSourceUri, mDestinationFile); + } else { + newSourceUri = mSourceUri; + } + // Stopgap fix for low-memory devices. + while (noBitmap) { + try { + updateProgress(); + // Try to do bitmap operations, downsample if low-memory + Bitmap bitmap = ImageLoader.loadOrientedBitmapWithBackouts(mContext, newSourceUri, + sampleSize); + if (bitmap == null) { + return null; + } + updateProgress(); + CachingPipeline pipeline = new CachingPipeline(FiltersManager.getManager(), + "Saving"); + + bitmap = pipeline.renderFinalImage(bitmap, preset); + updateProgress(); + + Object xmp = getPanoramaXMPData(mSelectedImageUri, preset); + ExifInterface exif = getExifData(mSelectedImageUri); + + // Set tags + long time = System.currentTimeMillis(); + exif.addDateTimeStampTag(ExifInterface.TAG_DATE_TIME, time, + TimeZone.getDefault()); + exif.setTag(exif.buildTag(ExifInterface.TAG_ORIENTATION, + ExifInterface.Orientation.TOP_LEFT)); + + // Remove old thumbnail + exif.removeCompressedThumbnail(); + + // If we succeed in writing the bitmap as a jpeg, return a uri. + if (putExifData(mDestinationFile, exif, bitmap)) { + putPanoramaXMPData(mDestinationFile, xmp); + uri = SaveImage.insertContent(mContext, mSelectedImageUri, mDestinationFile, + time); + } + updateProgress(); + + // mDestinationFile will save the newSourceUri info in the XMP. + XmpPresets.writeFilterXMP(mContext, newSourceUri, mDestinationFile, preset); + updateProgress(); + + // Since we have a new image inserted to media store, we can + // safely remove the old one which is selected by the user. + // TODO: we should fix that, do an update instead of insert+remove, + // as well as asking Gallery to update its cached version of the image + if (USE_AUX_DIR) { + removeSelectedImage(); + } + updateProgress(); + noBitmap = false; + UsageStatistics.onEvent(UsageStatistics.COMPONENT_EDITOR, + "SaveComplete", null); + } catch (OutOfMemoryError e) { + // Try 5 times before failing for good. + if (++num_tries >= 5) { + throw e; + } + System.gc(); + sampleSize *= 2; + resetProgress(); + } + } + return uri; + } + + private void removeSelectedImage() { + String scheme = mSelectedImageUri.getScheme(); + if (scheme != null && scheme.equals(ContentResolver.SCHEME_CONTENT)) { + if (mSelectedImageUri.getAuthority().equals(MediaStore.AUTHORITY)) { + mContext.getContentResolver().delete(mSelectedImageUri, null, null); + } + } + } + + /** + * 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 = getLocalFileFromUri(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; + } + + public static Uri makeAndInsertUri(Context context, Uri sourceUri) { + long time = System.currentTimeMillis(); + String filename = new SimpleDateFormat(TIME_STAMP_NAME).format(new Date(time)); + File saveDirectory = getFinalSaveDirectory(context, sourceUri); + File file = new File(saveDirectory, filename + ".JPG"); + return insertContent(context, sourceUri, file, time); + } + + public static void saveImage(ImagePreset preset, final FilterShowActivity filterShowActivity, + File destination) { + Uri selectedImageUri = filterShowActivity.getSelectedImageUri(); + Uri sourceImageUri = MasterImage.getImage().getUri(); + + Intent processIntent = ProcessingService.getSaveIntent(filterShowActivity, preset, + destination, selectedImageUri, sourceImageUri); + + filterShowActivity.startService(processIntent); + + if (!filterShowActivity.isSimpleEditAction()) { + // terminate for now + filterShowActivity.completeSaveImage(selectedImageUri); + } + } + + public 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, + 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) { + File file = getLocalFileFromUri(context, sourceUri); + if (file != null) { + return file.getParentFile(); + } else { + return null; + } + } + + /** + * 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 getLocalFileFromUri(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]; + } + + /** + * Gets the actual filename for a Uri from Gallery's ContentProvider. + */ + private static String getTrueFilename(Context context, Uri src) { + if (context == null || src == null) { + return null; + } + final String[] trueName = new String[1]; + querySource(context, src, new String[] { + ImageColumns.DATA + }, new ContentResolverQueryCallback() { + @Override + public void onCursorResult(Cursor cursor) { + trueName[0] = new File(cursor.getString(0)).getName(); + } + }); + return trueName[0]; + } + + /** + * Checks whether the true filename has the panorama image prefix. + */ + private static boolean hasPanoPrefix(Context context, Uri src) { + String name = getTrueFilename(context, src); + return name != null && name.startsWith(PREFIX_PANO); + } + + /** + * Insert the content (saved file) with proper source photo properties. + */ + public static Uri insertContent(Context context, Uri sourceUri, File file, long time) { + time /= 1000; + + final ContentValues values = new ContentValues(); + 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_ADDED, time); + 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, + }; + SaveImage.querySource(context, sourceUri, projection, + new SaveImage.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); + } + +} diff --git a/src/com/android/gallery3d/util/SaveVideoFileUtils.java b/src/com/android/gallery3d/util/SaveVideoFileUtils.java index da0970b1d..10c41de90 100644 --- a/src/com/android/gallery3d/util/SaveVideoFileUtils.java +++ b/src/com/android/gallery3d/util/SaveVideoFileUtils.java @@ -25,7 +25,7 @@ import android.os.Environment; import android.provider.MediaStore.Video; import android.provider.MediaStore.Video.VideoColumns; -import com.android.gallery3d.filtershow.tools.SaveCopyTask.ContentResolverQueryCallback; +import com.android.gallery3d.filtershow.tools.SaveImage.ContentResolverQueryCallback; import java.io.File; import java.sql.Date; -- cgit v1.2.3