/* * 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.SaveCopyTask; 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 { 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); 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 = SaveCopyTask.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 { 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; } }