2 * Copyright (C) 2013 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.android.gallery3d.filtershow.crop;
19 import android.app.ActionBar;
20 import android.app.Activity;
21 import android.app.WallpaperManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.graphics.Bitmap;
25 import android.graphics.Bitmap.CompressFormat;
26 import android.graphics.BitmapFactory;
27 import android.graphics.BitmapRegionDecoder;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.net.Uri;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.provider.MediaStore;
34 import android.util.DisplayMetrics;
35 import android.util.Log;
36 import android.view.View;
37 import android.view.View.OnClickListener;
38 import android.view.WindowManager;
39 import android.widget.Toast;
41 import com.android.gallery3d.R;
43 import java.io.ByteArrayInputStream;
44 import java.io.ByteArrayOutputStream;
45 import java.io.FileNotFoundException;
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.OutputStream;
51 * Activity for cropping an image.
53 public class CropActivity extends Activity {
54 private static final String LOGTAG = "CropActivity";
55 public static final String CROP_ACTION = "com.android.camera.action.CROP";
56 private CropExtras mCropExtras = null;
57 private LoadBitmapTask mLoadBitmapTask = null;
59 private Bitmap mOriginalBitmap = null;
60 private RectF mOriginalBounds = null;
61 private Uri mSourceUri = null;
62 private CropView mCropView = null;
63 private View mSaveButton = null;
64 private boolean finalIOGuard = false;
66 private Intent mResultIntent = null;
67 private static final int SELECT_PICTURE = 1; // request code for picker
69 private static final int DEFAULT_COMPRESS_QUALITY = 90;
70 public static final int MAX_BMAP_IN_INTENT = 990000;
73 private static final int DO_SET_WALLPAPER = 1;
74 private static final int DO_RETURN_DATA = 1 << 1;
75 private static final int DO_EXTRA_OUTPUT = 1 << 2;
77 private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
80 public void onCreate(Bundle savedInstanceState) {
81 super.onCreate(savedInstanceState);
82 Intent intent = getIntent();
83 mResultIntent = new Intent();
84 setResult(RESULT_CANCELED, mResultIntent);
85 mCropExtras = getExtrasFromIntent(intent);
86 if (mCropExtras != null && mCropExtras.getShowWhenLocked()) {
87 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
90 setContentView(R.layout.crop_activity);
91 mCropView = (CropView) findViewById(R.id.cropView);
93 ActionBar actionBar = getActionBar();
94 actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
95 actionBar.setCustomView(R.layout.filtershow_actionbar);
97 View mSaveButton = actionBar.getCustomView();
98 mSaveButton.setOnClickListener(new OnClickListener() {
100 public void onClick(View view) {
105 if (intent.getData() != null) {
106 mSourceUri = intent.getData();
107 startLoadBitmap(mSourceUri);
113 private void enableSave(boolean enable) {
114 if (mSaveButton != null) {
115 mSaveButton.setEnabled(enable);
120 protected void onDestroy() {
121 if (mLoadBitmapTask != null) {
122 mLoadBitmapTask.cancel(false);
128 * Opens a selector in Gallery to chose an image for use when none was given
129 * in the CROP intent.
131 private void pickImage() {
132 Intent intent = new Intent();
133 intent.setType("image/*");
134 intent.setAction(Intent.ACTION_GET_CONTENT);
135 startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)),
140 * Callback for pickImage().
143 public void onActivityResult(int requestCode, int resultCode, Intent data) {
144 if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) {
145 mSourceUri = data.getData();
146 startLoadBitmap(mSourceUri);
151 * Gets screen size metric.
153 private int getScreenImageSize() {
154 DisplayMetrics outMetrics = new DisplayMetrics();
155 getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
156 return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
160 * Method that loads a bitmap in an async task.
162 private void startLoadBitmap(Uri uri) {
164 final View loading = findViewById(R.id.loading);
165 loading.setVisibility(View.VISIBLE);
166 mLoadBitmapTask = new LoadBitmapTask();
167 mLoadBitmapTask.execute(uri);
171 * Method called on UI thread with loaded bitmap.
173 private void doneLoadBitmap(Bitmap bitmap, RectF bounds) {
174 final View loading = findViewById(R.id.loading);
175 loading.setVisibility(View.GONE);
176 mOriginalBitmap = bitmap;
177 mOriginalBounds = bounds;
178 if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
179 RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
180 mCropView.initialize(bitmap, imgBounds, imgBounds, 0);
183 Log.w(LOGTAG, "could not load image for cropping");
185 setResult(RESULT_CANCELED, new Intent());
191 * Display toast for image loading failure.
193 private void cannotLoadImage() {
194 CharSequence text = getString(R.string.cannot_load_image);
195 Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
200 * AsyncTask for loading a bitmap into memory.
202 * @see #startLoadBitmap(Uri)
203 * @see #doneLoadBitmap(Bitmap)
205 private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> {
208 Rect mOriginalBounds;
210 public LoadBitmapTask() {
211 mBitmapSize = getScreenImageSize();
212 mContext = getApplicationContext();
213 mOriginalBounds = new Rect();
217 protected Bitmap doInBackground(Uri... params) {
218 Bitmap bmap = CropLoader.getConstrainedBitmap(params[0], mContext, mBitmapSize,
224 protected void onPostExecute(Bitmap result) {
225 doneLoadBitmap(result, new RectF(mOriginalBounds));
229 private void startFinishOutput() {
236 Uri destinationUri = null;
238 if (mOriginalBitmap != null && mCropExtras != null) {
239 if (mCropExtras.getExtraOutput() != null) {
240 destinationUri = mCropExtras.getExtraOutput();
241 flags |= DO_EXTRA_OUTPUT;
243 if (mCropExtras.getSetAsWallpaper()) {
244 flags |= DO_SET_WALLPAPER;
246 if (mCropExtras.getReturnData()) {
247 flags |= DO_RETURN_DATA;
251 destinationUri = CropLoader.makeAndInsertUri(this, mSourceUri);
252 if (destinationUri != null) {
253 flags |= DO_EXTRA_OUTPUT;
256 if ((flags & FLAG_CHECK) != 0) {
257 RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
258 RectF crop = getBitmapCrop(photo);
259 startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
260 photo, mOriginalBounds,
261 (mCropExtras == null) ? null : mCropExtras.getOutputFormat());
264 setResult(RESULT_CANCELED, mResultIntent);
269 private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
270 RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format) {
271 if (cropBounds == null || photoBounds == null || currentBitmap == null
272 || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
273 || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
274 || photoBounds.height() == 0) {
277 if ((flags & FLAG_CHECK) == 0) {
278 return; // no output options
280 if ((flags & DO_SET_WALLPAPER) != 0) {
281 Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
284 final View loading = findViewById(R.id.loading);
285 loading.setVisibility(View.VISIBLE);
286 BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
287 photoBounds, currentBitmapBounds);
288 ioTask.execute(currentBitmap);
291 private void doneBitmapIO(boolean success, Intent intent) {
292 final View loading = findViewById(R.id.loading);
293 loading.setVisibility(View.GONE);
295 setResult(RESULT_OK, intent);
297 setResult(RESULT_CANCELED, intent);
302 private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
304 private final WallpaperManager mWPManager;
305 InputStream mInStream = null;
306 OutputStream mOutStream = null;
307 String mOutputFormat = null;
314 Intent mResultIntent = null;
316 public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
317 RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds) {
318 mOutputFormat = outputFormat;
324 mPhoto = photoBounds;
325 mOrig = originalBitmapBounds;
326 mWPManager = WallpaperManager.getInstance(getApplicationContext());
327 mResultIntent = new Intent();
329 if ((flags & DO_EXTRA_OUTPUT) != 0) {
330 if (mOutUri == null) {
331 Log.w(LOGTAG, "cannot write file, no output URI given");
334 mOutStream = getContentResolver().openOutputStream(mOutUri);
335 } catch (FileNotFoundException e) {
336 Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
341 if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
342 if (mInUri == null) {
343 Log.w(LOGTAG, "cannot read original file, no input URI given");
346 mInStream = getContentResolver().openInputStream(mInUri);
347 } catch (FileNotFoundException e) {
348 Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
355 protected Boolean doInBackground(Bitmap... params) {
356 boolean failure = false;
357 Bitmap img = params[0];
359 // Set extra for crop bounds
360 if (mCrop != null && mPhoto != null && mOrig != null) {
361 RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
362 if (trueCrop != null) {
363 Rect rounded = new Rect();
364 trueCrop.roundOut(rounded);
365 mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
369 // Find the small cropped bitmap that is returned in the intent
370 if ((mFlags & DO_RETURN_DATA) != 0) {
371 assert (img != null);
372 Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
374 ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
377 Log.w(LOGTAG, "could not downsample bitmap to return in data");
380 mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
384 // Do the large cropped bitmap and/or set the wallpaper
385 if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
386 BitmapRegionDecoder decoder = null;
388 decoder = BitmapRegionDecoder.newInstance(mInStream, true);
389 } catch (IOException e) {
390 Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
392 if (decoder == null) {
397 // Find crop bounds (scaled to original image size)
398 RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
399 if (trueCrop == null) {
400 Log.w(LOGTAG, "cannot find crop for full size image");
404 Rect roundedTrueCrop = new Rect();
405 trueCrop.roundOut(roundedTrueCrop);
407 if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
408 Log.w(LOGTAG, "crop has bad values for full size image");
412 // Do region decoding to get crop bitmap
413 BitmapFactory.Options options = new BitmapFactory.Options();
414 options.inMutable = true;
415 Bitmap crop = decoder.decodeRegion(roundedTrueCrop, options);
419 Log.w(LOGTAG, "cannot region decode file: " + mInUri.toString());
423 // Get output compression format
425 convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
427 // If we only need to output to a URI, compress straight to file
428 if (mFlags == DO_EXTRA_OUTPUT) {
429 if (mOutStream == null
430 || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
431 Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
435 // Compress to byte array
436 ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
437 if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
439 // If we need to output to a Uri, write compressed
441 if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
442 if (mOutStream == null) {
444 "failed to compress bitmap to file: " + mOutUri.toString());
448 mOutStream.write(tmpOut.toByteArray());
449 } catch (IOException e) {
451 "failed to compress bitmap to file: "
452 + mOutUri.toString(), e);
458 // If we need to set to the wallpaper, set it
459 if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
460 if (mWPManager == null) {
461 Log.w(LOGTAG, "no wallpaper manager");
465 mWPManager.setStream(new ByteArrayInputStream(tmpOut
467 } catch (IOException e) {
468 Log.w(LOGTAG, "cannot write stream to wallpaper", e);
474 Log.w(LOGTAG, "cannot compress bitmap");
479 return !failure; // True if any of the operations failed
483 protected void onPostExecute(Boolean result) {
484 doneBitmapIO(result.booleanValue(), mResultIntent);
489 private void done() {
493 protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
494 RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
495 RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
499 Rect intCrop = new Rect();
500 crop.roundOut(intCrop);
501 return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
505 protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
506 if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
507 throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
510 int size = CropMath.getBitmapSize(image);
511 while (size > max_size) {
515 Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
516 image.getHeight() >> shifts, true);
520 // Handle edge case for rounding.
521 if (CropMath.getBitmapSize(ret) > max_size) {
522 return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
528 * Gets the crop extras from the intent, or null if none exist.
530 protected static CropExtras getExtrasFromIntent(Intent intent) {
531 Bundle extras = intent.getExtras();
532 if (extras != null) {
533 return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0),
534 extras.getInt(CropExtras.KEY_OUTPUT_Y, 0),
535 extras.getBoolean(CropExtras.KEY_SCALE, true) &&
536 extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false),
537 extras.getInt(CropExtras.KEY_ASPECT_X, 0),
538 extras.getInt(CropExtras.KEY_ASPECT_Y, 0),
539 extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false),
540 extras.getBoolean(CropExtras.KEY_RETURN_DATA, false),
541 (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT),
542 extras.getString(CropExtras.KEY_OUTPUT_FORMAT),
543 extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false),
544 extras.getFloat(CropExtras.KEY_SPOTLIGHT_X),
545 extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y));
550 protected static CompressFormat convertExtensionToCompressFormat(String extension) {
551 return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
554 protected static String getFileExtension(String requestFormat) {
555 String outputFormat = (requestFormat == null)
558 outputFormat = outputFormat.toLowerCase();
559 return (outputFormat.equals("png") || outputFormat.equals("gif"))
560 ? "png" // We don't support gif compression.
564 private RectF getBitmapCrop(RectF imageBounds) {
565 RectF crop = mCropView.getCrop();
566 RectF photo = mCropView.getPhoto();
567 if (crop == null || photo == null) {
568 Log.w(LOGTAG, "could not get crop");
571 RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);