diff options
Diffstat (limited to 'src/com/android/gallery3d/filtershow/crop/CropActivity.java')
-rw-r--r-- | src/com/android/gallery3d/filtershow/crop/CropActivity.java | 697 |
1 files changed, 697 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java new file mode 100644 index 000000000..0a0c36703 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -0,0 +1,697 @@ +/* + * 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.crop; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +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.SaveImage; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Activity for cropping an image. + */ +public class CropActivity extends Activity { + private static final String LOGTAG = "CropActivity"; + public static final String CROP_ACTION = "com.android.camera.action.CROP"; + private CropExtras mCropExtras = null; + private LoadBitmapTask mLoadBitmapTask = null; + + private int mOutputX = 0; + private int mOutputY = 0; + private Bitmap mOriginalBitmap = null; + private RectF mOriginalBounds = null; + private int mOriginalRotation = 0; + private Uri mSourceUri = null; + private CropView mCropView = null; + private View mSaveButton = null; + private boolean finalIOGuard = false; + + private static final int SELECT_PICTURE = 1; // request code for picker + + private static final int DEFAULT_COMPRESS_QUALITY = 90; + /** + * The maximum bitmap size we allow to be returned through the intent. + * Intents have a maximum of 1MB in total size. However, the Bitmap seems to + * have some overhead to hit so that we go way below the limit here to make + * sure the intent stays below 1MB.We should consider just returning a byte + * array instead of a Bitmap instance to avoid overhead. + */ + public static final int MAX_BMAP_IN_INTENT = 750000; + + // Flags + private static final int DO_SET_WALLPAPER = 1; + private static final int DO_RETURN_DATA = 1 << 1; + private static final int DO_EXTRA_OUTPUT = 1 << 2; + + private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + setResult(RESULT_CANCELED, new Intent()); + mCropExtras = getExtrasFromIntent(intent); + if (mCropExtras != null && mCropExtras.getShowWhenLocked()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + + setContentView(R.layout.crop_activity); + mCropView = (CropView) findViewById(R.id.cropView); + + ActionBar actionBar = getActionBar(); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + actionBar.setCustomView(R.layout.filtershow_actionbar); + + View mSaveButton = actionBar.getCustomView(); + mSaveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + startFinishOutput(); + } + }); + + if (intent.getData() != null) { + mSourceUri = intent.getData(); + startLoadBitmap(mSourceUri); + } else { + pickImage(); + } + } + + private void enableSave(boolean enable) { + if (mSaveButton != null) { + mSaveButton.setEnabled(enable); + } + } + + @Override + protected void onDestroy() { + if (mLoadBitmapTask != null) { + mLoadBitmapTask.cancel(false); + } + super.onDestroy(); + } + + @Override + public void onConfigurationChanged (Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mCropView.configChanged(); + } + + /** + * Opens a selector in Gallery to chose an image for use when none was given + * in the CROP intent. + */ + private void pickImage() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)), + SELECT_PICTURE); + } + + /** + * Callback for pickImage(). + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) { + mSourceUri = data.getData(); + startLoadBitmap(mSourceUri); + } + } + + /** + * Gets screen size metric. + */ + private int getScreenImageSize() { + DisplayMetrics outMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(outMetrics); + return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels); + } + + /** + * Method that loads a bitmap in an async task. + */ + private void startLoadBitmap(Uri uri) { + if (uri != null) { + enableSave(false); + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + mLoadBitmapTask = new LoadBitmapTask(); + mLoadBitmapTask.execute(uri); + } else { + cannotLoadImage(); + done(); + } + } + + /** + * Method called on UI thread with loaded bitmap. + */ + private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) { + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + mOriginalBitmap = bitmap; + mOriginalBounds = bounds; + mOriginalRotation = orientation; + if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) { + RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + mCropView.initialize(bitmap, imgBounds, imgBounds, orientation); + if (mCropExtras != null) { + int aspectX = mCropExtras.getAspectX(); + int aspectY = mCropExtras.getAspectY(); + mOutputX = mCropExtras.getOutputX(); + mOutputY = mCropExtras.getOutputY(); + if (mOutputX > 0 && mOutputY > 0) { + mCropView.applyAspect(mOutputX, mOutputY); + + } + float spotX = mCropExtras.getSpotlightX(); + float spotY = mCropExtras.getSpotlightY(); + if (spotX > 0 && spotY > 0) { + mCropView.setWallpaperSpotlight(spotX, spotY); + } + if (aspectX > 0 && aspectY > 0) { + mCropView.applyAspect(aspectX, aspectY); + } + } + enableSave(true); + } else { + Log.w(LOGTAG, "could not load image for cropping"); + cannotLoadImage(); + setResult(RESULT_CANCELED, new Intent()); + done(); + } + } + + /** + * Display toast for image loading failure. + */ + private void cannotLoadImage() { + CharSequence text = getString(R.string.cannot_load_image); + Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); + toast.show(); + } + + /** + * AsyncTask for loading a bitmap into memory. + * + * @see #startLoadBitmap(Uri) + * @see #doneLoadBitmap(Bitmap) + */ + private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> { + int mBitmapSize; + Context mContext; + Rect mOriginalBounds; + int mOrientation; + + public LoadBitmapTask() { + mBitmapSize = getScreenImageSize(); + mContext = getApplicationContext(); + mOriginalBounds = new Rect(); + mOrientation = 0; + } + + @Override + protected Bitmap doInBackground(Uri... params) { + Uri uri = params[0]; + Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize, + mOriginalBounds, false); + mOrientation = ImageLoader.getMetadataRotation(mContext, uri); + return bmap; + } + + @Override + protected void onPostExecute(Bitmap result) { + doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation); + } + } + + private void startFinishOutput() { + if (finalIOGuard) { + return; + } else { + finalIOGuard = true; + } + enableSave(false); + Uri destinationUri = null; + int flags = 0; + if (mOriginalBitmap != null && mCropExtras != null) { + if (mCropExtras.getExtraOutput() != null) { + destinationUri = mCropExtras.getExtraOutput(); + if (destinationUri != null) { + flags |= DO_EXTRA_OUTPUT; + } + } + if (mCropExtras.getSetAsWallpaper()) { + flags |= DO_SET_WALLPAPER; + } + if (mCropExtras.getReturnData()) { + flags |= DO_RETURN_DATA; + } + } + if (flags == 0) { + destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri); + if (destinationUri != null) { + flags |= DO_EXTRA_OUTPUT; + } + } + if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) { + RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight()); + RectF crop = getBitmapCrop(photo); + startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop, + photo, mOriginalBounds, + (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation); + return; + } + setResult(RESULT_CANCELED, new Intent()); + done(); + return; + } + + private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri, + RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format, + int rotation) { + if (cropBounds == null || photoBounds == null || currentBitmap == null + || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0 + || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0 + || photoBounds.height() == 0) { + return; // fail fast + } + if ((flags & FLAG_CHECK) == 0) { + return; // no output options + } + if ((flags & DO_SET_WALLPAPER) != 0) { + Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show(); + } + + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds, + photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY); + ioTask.execute(currentBitmap); + } + + private void doneBitmapIO(boolean success, Intent intent) { + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + if (success) { + setResult(RESULT_OK, intent); + } else { + setResult(RESULT_CANCELED, intent); + } + done(); + } + + private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> { + + private final WallpaperManager mWPManager; + InputStream mInStream = null; + OutputStream mOutStream = null; + String mOutputFormat = null; + Uri mOutUri = null; + Uri mInUri = null; + int mFlags = 0; + RectF mCrop = null; + RectF mPhoto = null; + RectF mOrig = null; + Intent mResultIntent = null; + int mRotation = 0; + + // Helper to setup input stream + private void regenerateInputStream() { + if (mInUri == null) { + Log.w(LOGTAG, "cannot read original file, no input URI given"); + } else { + Utils.closeSilently(mInStream); + try { + mInStream = getContentResolver().openInputStream(mInUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e); + } + } + } + + public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags, + RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation, + int outputX, int outputY) { + mOutputFormat = outputFormat; + mOutStream = null; + mOutUri = destUri; + mInUri = sourceUri; + mFlags = flags; + mCrop = cropBounds; + mPhoto = photoBounds; + mOrig = originalBitmapBounds; + mWPManager = WallpaperManager.getInstance(getApplicationContext()); + mResultIntent = new Intent(); + mRotation = (rotation < 0) ? -rotation : rotation; + mRotation %= 360; + mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90 + mOutputX = outputX; + mOutputY = outputY; + + if ((flags & DO_EXTRA_OUTPUT) != 0) { + if (mOutUri == null) { + Log.w(LOGTAG, "cannot write file, no output URI given"); + } else { + try { + mOutStream = getContentResolver().openOutputStream(mOutUri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e); + } + } + } + + if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) { + regenerateInputStream(); + } + } + + @Override + protected Boolean doInBackground(Bitmap... params) { + boolean failure = false; + Bitmap img = params[0]; + + // Set extra for crop bounds + if (mCrop != null && mPhoto != null && mOrig != null) { + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + Matrix m = new Matrix(); + m.setRotate(mRotation); + m.mapRect(trueCrop); + if (trueCrop != null) { + Rect rounded = new Rect(); + trueCrop.roundOut(rounded); + mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded); + } + } + + // Find the small cropped bitmap that is returned in the intent + if ((mFlags & DO_RETURN_DATA) != 0) { + assert (img != null); + Bitmap ret = getCroppedImage(img, mCrop, mPhoto); + if (ret != null) { + ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT); + } + if (ret == null) { + Log.w(LOGTAG, "could not downsample bitmap to return in data"); + failure = true; + } else { + if (mRotation > 0) { + Matrix m = new Matrix(); + m.setRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(), + ret.getHeight(), m, true); + if (tmp != null) { + ret = tmp; + } + } + mResultIntent.putExtra(CropExtras.KEY_DATA, ret); + } + } + + // Do the large cropped bitmap and/or set the wallpaper + if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) { + // Find crop bounds (scaled to original image size) + RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig); + if (trueCrop == null) { + Log.w(LOGTAG, "cannot find crop for full size image"); + failure = true; + return false; + } + Rect roundedTrueCrop = new Rect(); + trueCrop.roundOut(roundedTrueCrop); + + if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) { + Log.w(LOGTAG, "crop has bad values for full size image"); + failure = true; + return false; + } + + // Attempt to open a region decoder + BitmapRegionDecoder decoder = null; + try { + decoder = BitmapRegionDecoder.newInstance(mInStream, true); + } catch (IOException e) { + Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e); + } + + Bitmap crop = null; + if (decoder != null) { + // Do region decoding to get crop bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + crop = decoder.decodeRegion(roundedTrueCrop, options); + decoder.recycle(); + } + + if (crop == null) { + // BitmapRegionDecoder has failed, try to crop in-memory + regenerateInputStream(); + Bitmap fullSize = null; + if (mInStream != null) { + fullSize = BitmapFactory.decodeStream(mInStream); + } + if (fullSize != null) { + crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left, + roundedTrueCrop.top, roundedTrueCrop.width(), + roundedTrueCrop.height()); + } + } + + if (crop == null) { + Log.w(LOGTAG, "cannot decode file: " + mInUri.toString()); + failure = true; + return false; + } + if (mOutputX > 0 && mOutputY > 0) { + Matrix m = new Matrix(); + RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight()); + if (mRotation > 0) { + m.setRotate(mRotation); + m.mapRect(cropRect); + } + RectF returnRect = new RectF(0, 0, mOutputX, mOutputY); + m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL); + m.preRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(), + (int) returnRect.height(), Bitmap.Config.ARGB_8888); + if (tmp != null) { + Canvas c = new Canvas(tmp); + c.drawBitmap(crop, m, new Paint()); + crop = tmp; + } + } else if (mRotation > 0) { + Matrix m = new Matrix(); + m.setRotate(mRotation); + Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(), + crop.getHeight(), m, true); + if (tmp != null) { + crop = tmp; + } + } + // Get output compression format + CompressFormat cf = + convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); + + // If we only need to output to a URI, compress straight to file + if (mFlags == DO_EXTRA_OUTPUT) { + if (mOutStream == null + || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) { + Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } else { + mResultIntent.setData(mOutUri); + } + } else { + // Compress to byte array + ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048); + if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) { + + // If we need to output to a Uri, write compressed + // bitmap out + if ((mFlags & DO_EXTRA_OUTPUT) != 0) { + if (mOutStream == null) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + mOutUri.toString()); + failure = true; + } else { + try { + mOutStream.write(tmpOut.toByteArray()); + mResultIntent.setData(mOutUri); + } catch (IOException e) { + Log.w(LOGTAG, + "failed to compress bitmap to file: " + + mOutUri.toString(), e); + failure = true; + } + } + } + + // If we need to set to the wallpaper, set it + if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) { + if (mWPManager == null) { + Log.w(LOGTAG, "no wallpaper manager"); + failure = true; + } else { + try { + mWPManager.setStream(new ByteArrayInputStream(tmpOut + .toByteArray())); + } catch (IOException e) { + Log.w(LOGTAG, "cannot write stream to wallpaper", e); + failure = true; + } + } + } + } else { + Log.w(LOGTAG, "cannot compress bitmap"); + failure = true; + } + } + } + return !failure; // True if any of the operations failed + } + + @Override + protected void onPostExecute(Boolean result) { + Utils.closeSilently(mOutStream); + Utils.closeSilently(mInStream); + doneBitmapIO(result.booleanValue(), mResultIntent); + } + + } + + private void done() { + finish(); + } + + protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) { + RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight()); + RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds); + if (crop == null) { + return null; + } + Rect intCrop = new Rect(); + crop.roundOut(intCrop); + return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(), + intCrop.height()); + } + + protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) { + if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) { + throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()"); + } + int shifts = 0; + int size = CropMath.getBitmapSize(image); + while (size > max_size) { + shifts++; + size /= 4; + } + Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts, + image.getHeight() >> shifts, true); + if (ret == null) { + return null; + } + // Handle edge case for rounding. + if (CropMath.getBitmapSize(ret) > max_size) { + return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true); + } + return ret; + } + + /** + * Gets the crop extras from the intent, or null if none exist. + */ + protected static CropExtras getExtrasFromIntent(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras != null) { + return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0), + extras.getInt(CropExtras.KEY_OUTPUT_Y, 0), + extras.getBoolean(CropExtras.KEY_SCALE, true) && + extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false), + extras.getInt(CropExtras.KEY_ASPECT_X, 0), + extras.getInt(CropExtras.KEY_ASPECT_Y, 0), + extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false), + extras.getBoolean(CropExtras.KEY_RETURN_DATA, false), + (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT), + extras.getString(CropExtras.KEY_OUTPUT_FORMAT), + extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_X), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y)); + } + return null; + } + + protected static CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; + } + + protected static String getFileExtension(String requestFormat) { + String outputFormat = (requestFormat == null) + ? "jpg" + : requestFormat; + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } + + private RectF getBitmapCrop(RectF imageBounds) { + RectF crop = mCropView.getCrop(); + RectF photo = mCropView.getPhoto(); + if (crop == null || photo == null) { + Log.w(LOGTAG, "could not get crop"); + return null; + } + RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds); + return scaledCrop; + } +} |