Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      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
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     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.
     15  */
     16 
     17 package com.cooliris.media;
     18 
     19 import android.app.Activity;
     20 import android.app.ProgressDialog;
     21 import android.content.ContentResolver;
     22 import android.content.ContentValues;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.graphics.Bitmap;
     27 import android.graphics.Canvas;
     28 import android.graphics.Matrix;
     29 import android.graphics.Path;
     30 import android.graphics.PointF;
     31 import android.graphics.PorterDuff;
     32 import android.graphics.Rect;
     33 import android.graphics.RectF;
     34 import android.graphics.Region;
     35 import android.media.ExifInterface;
     36 import android.media.FaceDetector;
     37 import android.media.MediaScannerConnection;
     38 import android.net.Uri;
     39 import android.os.Bundle;
     40 import android.os.Handler;
     41 import android.provider.MediaStore;
     42 import android.provider.MediaStore.Images.ImageColumns;
     43 import android.util.AttributeSet;
     44 import android.util.Log;
     45 import android.view.MotionEvent;
     46 import android.view.View;
     47 import android.view.Window;
     48 import android.view.WindowManager;
     49 import android.widget.Toast;
     50 
     51 import java.io.File;
     52 import java.io.IOException;
     53 import java.io.InputStream;
     54 import java.io.OutputStream;
     55 import java.net.URISyntaxException;
     56 import java.util.ArrayList;
     57 import java.util.HashMap;
     58 import java.util.concurrent.CountDownLatch;
     59 
     60 import com.cooliris.app.App;
     61 import com.cooliris.app.Res;
     62 
     63 /**
     64  * The activity can crop specific region of interest from an image.
     65  */
     66 public class CropImage extends MonitoredActivity {
     67     private static final String TAG = "CropImage";
     68 
     69 	public static final int CROP_MSG = 10;
     70     public static final int CROP_MSG_INTERNAL = 100;
     71 
     72     private App mApp = null;
     73 
     74     // These are various options can be specified in the intent.
     75     private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG; // only
     76                                                                               // used
     77                                                                               // with
     78                                                                               // mSaveUri
     79     private Uri mSaveUri = null;
     80     private int mAspectX, mAspectY; // CR: two definitions per line == sad
     81                                     // panda.
     82     private boolean mDoFaceDetection = true;
     83     private boolean mCircleCrop = false;
     84     private final Handler mHandler = new Handler();
     85 
     86     // These options specifiy the output image size and whether we should
     87     // scale the output to fit it (or just crop it).
     88     private int mOutputX, mOutputY;
     89     private boolean mScale;
     90     private boolean mScaleUp = true;
     91 
     92     boolean mWaitingToPick; // Whether we are wait the user to pick a face.
     93     boolean mSaving; // Whether the "save" button is already clicked.
     94 
     95     private CropImageView mImageView;
     96     private ContentResolver mContentResolver;
     97 
     98     private Bitmap mBitmap;
     99     private MediaItem mItem;
    100     private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet();
    101     HighlightView mCrop;
    102 
    103     static private final HashMap<Context, MediaScannerConnection> mConnectionMap = new HashMap<Context, MediaScannerConnection>();
    104 
    105     static public void launchCropperOrFinish(final Context context, final MediaItem item) {
    106     	final Bundle myExtras = ((Activity) context).getIntent().getExtras();
    107     	String cropValue = myExtras != null ? myExtras.getString("crop") : null;
    108     	final String contentUri = item.mContentUri;
    109     	if (contentUri == null)
    110     		return;
    111     	if (cropValue != null) {
    112     		Bundle newExtras = new Bundle();
    113     		if (cropValue.equals("circle")) {
    114     			newExtras.putString("circleCrop", "true");
    115     		}
    116     		Intent cropIntent = new Intent();
    117     		cropIntent.setData(Uri.parse(contentUri));
    118     		cropIntent.setClass(context, CropImage.class);
    119     		cropIntent.putExtras(newExtras);
    120     		// Pass through any extras that were passed in.
    121     		cropIntent.putExtras(myExtras);
    122     		((Activity) context).startActivityForResult(cropIntent, CropImage.CROP_MSG);
    123     	} else {
    124     		if (contentUri.startsWith("http://")) {
    125     			// This is a http uri, we must save it locally first and
    126     			// generate a content uri from it.
    127     			final ProgressDialog dialog = ProgressDialog.show(context, context.getResources().getString(Res.string.initializing),
    128     					context.getResources().getString(Res.string.running_face_detection), true, false);
    129     			if (contentUri != null) {
    130     				MediaScannerConnection.MediaScannerConnectionClient client = new MediaScannerConnection.MediaScannerConnectionClient() {
    131     					public void onMediaScannerConnected() {
    132     						MediaScannerConnection connection = mConnectionMap.get(context);
    133     						if (connection != null) {
    134     							try {
    135     								final String path = UriTexture.writeHttpDataInDirectory(context, contentUri,
    136     										LocalDataSource.DOWNLOAD_BUCKET_NAME);
    137     								if (path != null) {
    138     									connection.scanFile(path, item.mMimeType);
    139     								} else {
    140     									shutdown("");
    141     								}
    142     							} catch (Exception e) {
    143     								shutdown("");
    144     							}
    145     						}
    146     					}
    147 
    148     					public void onScanCompleted(String path, Uri uri) {
    149     						shutdown(uri.toString());
    150     					}
    151 
    152     					public void shutdown(String uri) {
    153     						dialog.dismiss();
    154     						performReturn(context, myExtras, uri.toString());
    155     						MediaScannerConnection connection = mConnectionMap.get(context);
    156     						if (connection != null) {
    157     							connection.disconnect();
    158     							mConnectionMap.put(context, null);
    159     						}
    160     					}
    161     				};
    162     				MediaScannerConnection connection = new MediaScannerConnection(context, client);
    163     				mConnectionMap.put(context, connection);
    164     				connection.connect();
    165     			}
    166     		} else {
    167     			performReturn(context, myExtras, contentUri);
    168     		}
    169     	}
    170     }
    171 
    172     static private void performReturn(Context context, Bundle myExtras, String contentUri) {
    173     	Intent result = new Intent(null, Uri.parse(contentUri));
    174     	boolean resultSet = false;
    175         if (myExtras != null) {
    176             final Uri outputUri = (Uri)myExtras.getParcelable(MediaStore.EXTRA_OUTPUT);
    177             if (outputUri != null) {
    178                 Bundle extras = new Bundle();
    179                 OutputStream outputStream = null;
    180                 try {
    181                     outputStream = context.getContentResolver().openOutputStream(outputUri);
    182                     if (outputStream != null) {
    183                         InputStream inputStream = context.getContentResolver().openInputStream(Uri.parse(contentUri));
    184                         Utils.copyStream(inputStream, outputStream);
    185                         Util.closeSilently(inputStream);
    186                     }
    187                     ((Activity) context).setResult(Activity.RESULT_OK, new Intent(outputUri.toString())
    188                     .putExtras(extras));
    189                     resultSet = true;
    190                 } catch (Exception ex) {
    191                     Log.e(TAG, "Cannot save to uri " + outputUri.toString());
    192                 } finally {
    193                     Util.closeSilently(outputStream);
    194                 }
    195             }
    196         }
    197     	if (!resultSet && myExtras != null && myExtras.getBoolean("return-data")) {
    198     		// The size of a transaction should be below 100K.
    199     		Bitmap bitmap = null;
    200     		try {
    201     			bitmap = UriTexture.createFromUri(context, contentUri, 1024, 1024, 0, null);
    202     		} catch (IOException e) {
    203     			;
    204     		} catch (URISyntaxException e) {
    205     			;
    206     		}
    207     		if (bitmap != null) {
    208     			result.putExtra("data", bitmap);
    209     		}
    210     	}
    211     	if (!resultSet)
    212     	    ((Activity) context).setResult(Activity.RESULT_OK, result);
    213     	((Activity) context).finish();
    214     }
    215 
    216     @Override
    217     public void onCreate(Bundle icicle) {
    218         super.onCreate(icicle);
    219         mApp = new App(CropImage.this);
    220         mContentResolver = getContentResolver();
    221         requestWindowFeature(Window.FEATURE_NO_TITLE);
    222         setContentView(Res.layout.cropimage);
    223 
    224         mImageView = (CropImageView) findViewById(Res.id.image);
    225 
    226         // CR: remove TODO's.
    227         // TODO: we may need to show this indicator for the main gallery
    228         // application
    229         // MenuHelper.showStorageToast(this);
    230 
    231         Intent intent = getIntent();
    232         Bundle extras = intent.getExtras();
    233 
    234         if (extras != null) {
    235             if (extras.getString("circleCrop") != null) {
    236                 mCircleCrop = true;
    237                 mAspectX = 1;
    238                 mAspectY = 1;
    239             }
    240             mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT);
    241             if (mSaveUri != null) {
    242                 String outputFormatString = extras.getString("outputFormat");
    243                 if (outputFormatString != null) {
    244                     mOutputFormat = Bitmap.CompressFormat.valueOf(outputFormatString);
    245                 }
    246             }
    247             mBitmap = (Bitmap) extras.getParcelable("data");
    248             mAspectX = extras.getInt("aspectX");
    249             mAspectY = extras.getInt("aspectY");
    250             mOutputX = extras.getInt("outputX");
    251             mOutputY = extras.getInt("outputY");
    252             mScale = extras.getBoolean("scale", true);
    253             mScaleUp = extras.getBoolean("scaleUpIfNeeded", true);
    254             mDoFaceDetection = extras.containsKey("noFaceDetection") ? !extras.getBoolean("noFaceDetection") : true;
    255         }
    256 
    257         if (mBitmap == null) {
    258             // Create a MediaItem representing the URI.
    259             Uri target = intent.getData();
    260             String targetScheme = target.getScheme();
    261             int rotation = 0;
    262 
    263             if (targetScheme.equals("content")) {
    264                 mItem = LocalDataSource.createMediaItemFromUri(this, target, MediaItem.MEDIA_TYPE_IMAGE);
    265             }
    266             try {
    267                 if (mItem != null) {
    268                     mBitmap = UriTexture.createFromUri(this, mItem.mContentUri, 1024, 1024, 0, null);
    269                     rotation = (int) mItem.mRotation;
    270                 } else {
    271                     mBitmap = UriTexture.createFromUri(this, target.toString(), 1024, 1024, 0, null);
    272                     if (targetScheme.equals("file")) {
    273                         ExifInterface exif = new ExifInterface(target.getPath());
    274                         rotation = (int) Shared.exifOrientationToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION,
    275                                 ExifInterface.ORIENTATION_NORMAL));
    276                     }
    277                 }
    278             } catch (IOException e) {
    279             } catch (URISyntaxException e) {
    280             }
    281 
    282             if (mBitmap != null && rotation != 0f) {
    283                 mBitmap = Util.rotate(mBitmap, rotation);
    284             }
    285         }
    286 
    287         if (mBitmap == null) {
    288             Log.e(TAG, "Cannot load bitmap, exiting.");
    289             finish();
    290             return;
    291         }
    292 
    293         // Make UI fullscreen.
    294         getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
    295 
    296         findViewById(Res.id.discard).setOnClickListener(new View.OnClickListener() {
    297             public void onClick(View v) {
    298                 setResult(RESULT_CANCELED);
    299                 finish();
    300             }
    301         });
    302 
    303         findViewById(Res.id.save).setOnClickListener(new View.OnClickListener() {
    304             public void onClick(View v) {
    305                 onSaveClicked();
    306             }
    307         });
    308 
    309         startFaceDetection();
    310     }
    311 
    312     private void startFaceDetection() {
    313         if (isFinishing()) {
    314             return;
    315         }
    316 
    317         mImageView.setImageBitmapResetBase(mBitmap, true);
    318 
    319         Util.startBackgroundJob(this, null, getResources().getString(Res.string.running_face_detection), new Runnable() {
    320             public void run() {
    321                 final CountDownLatch latch = new CountDownLatch(1);
    322                 final Bitmap b = mBitmap;
    323                 mHandler.post(new Runnable() {
    324                     public void run() {
    325                         if (b != mBitmap && b != null) {
    326                             mImageView.setImageBitmapResetBase(b, true);
    327                             mBitmap.recycle();
    328                             mBitmap = b;
    329                         }
    330                         if (mImageView.getScale() == 1.0f) {
    331                             mImageView.center(true, true);
    332                         }
    333                         latch.countDown();
    334                     }
    335                 });
    336                 try {
    337                     latch.await();
    338                 } catch (InterruptedException e) {
    339                     throw new RuntimeException(e);
    340                 }
    341                 mRunFaceDetection.run();
    342             }
    343         }, mHandler);
    344     }
    345 
    346     private void onSaveClicked() {
    347         // CR: TODO!
    348         // TODO this code needs to change to use the decode/crop/encode single
    349         // step api so that we don't require that the whole (possibly large)
    350         // bitmap doesn't have to be read into memory
    351         if (mSaving)
    352             return;
    353 
    354         if (mCrop == null) {
    355             return;
    356         }
    357 
    358         mSaving = true;
    359 
    360         Rect r = mCrop.getCropRect();
    361 
    362         int width = r.width(); // CR: final == happy panda!
    363         int height = r.height();
    364 
    365         // If we are circle cropping, we want alpha channel, which is the
    366         // third param here.
    367         Bitmap croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
    368         {
    369             Canvas canvas = new Canvas(croppedImage);
    370             Rect dstRect = new Rect(0, 0, width, height);
    371             canvas.drawBitmap(mBitmap, r, dstRect, null);
    372         }
    373 
    374         if (mCircleCrop) {
    375             // OK, so what's all this about?
    376             // Bitmaps are inherently rectangular but we want to return
    377             // something that's basically a circle. So we fill in the
    378             // area around the circle with alpha. Note the all important
    379             // PortDuff.Mode.CLEARes.
    380             Canvas c = new Canvas(croppedImage);
    381             Path p = new Path();
    382             p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW);
    383             c.clipPath(p, Region.Op.DIFFERENCE);
    384             c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
    385         }
    386 
    387         // If the output is required to a specific size then scale or fill.
    388         if (mOutputX != 0 && mOutputY != 0) {
    389             if (mScale) {
    390                 // Scale the image to the required dimensions.
    391                 Bitmap old = croppedImage;
    392                 croppedImage = Util.transform(new Matrix(), croppedImage, mOutputX, mOutputY, mScaleUp);
    393                 if (old != croppedImage) {
    394                     old.recycle();
    395                 }
    396             } else {
    397 
    398                 /*
    399                  * Don't scale the image crop it to the size requested. Create
    400                  * an new image with the cropped image in the center and the
    401                  * extra space filled.
    402                  */
    403 
    404                 // Don't scale the image but instead fill it so it's the
    405                 // required dimension
    406                 Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565);
    407                 Canvas canvas = new Canvas(b);
    408 
    409                 Rect srcRect = mCrop.getCropRect();
    410                 Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);
    411 
    412                 int dx = (srcRect.width() - dstRect.width()) / 2;
    413                 int dy = (srcRect.height() - dstRect.height()) / 2;
    414 
    415                 // If the srcRect is too big, use the center part of it.
    416                 srcRect.inset(Math.max(0, dx), Math.max(0, dy));
    417 
    418                 // If the dstRect is too big, use the center part of it.
    419                 dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));
    420 
    421                 // Draw the cropped bitmap in the center.
    422                 canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
    423 
    424                 // Set the cropped bitmap as the new bitmap.
    425                 croppedImage.recycle();
    426                 croppedImage = b;
    427             }
    428         }
    429 
    430         // Return the cropped image directly or save it to the specified URI.
    431         Bundle myExtras = getIntent().getExtras();
    432         if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) {
    433             Bundle extras = new Bundle();
    434             extras.putParcelable("data", croppedImage);
    435             setResult(RESULT_OK, (new Intent()).setAction("inline-data").putExtras(extras));
    436             finish();
    437         } else {
    438             final Bitmap b = croppedImage;
    439             final Runnable save = new Runnable() {
    440                 public void run() {
    441                     saveOutput(b);
    442                 }
    443             };
    444             Util.startBackgroundJob(this, null, getResources().getString(Res.string.saving_image), save, mHandler);
    445         }
    446     }
    447 
    448     private void saveOutput(Bitmap croppedImage) {
    449         if (mSaveUri != null) {
    450             OutputStream outputStream = null;
    451             try {
    452                 outputStream = mContentResolver.openOutputStream(mSaveUri);
    453                 if (outputStream != null) {
    454                     croppedImage.compress(mOutputFormat, 75, outputStream);
    455                 }
    456                 // TODO ExifInterface write
    457             } catch (IOException ex) {
    458                 Log.e(TAG, "Cannot open file: " + mSaveUri, ex);
    459             } finally {
    460                 Util.closeSilently(outputStream);
    461             }
    462             Bundle extras = new Bundle();
    463             setResult(RESULT_OK, new Intent(mSaveUri.toString()).putExtras(extras));
    464         } else {
    465             Bundle extras = new Bundle();
    466             extras.putString("rect", mCrop.getCropRect().toString());
    467             if (mItem == null) {
    468                 // CR: Comments should be full sentences.
    469                 // this image doesn't belong to the local data source
    470                 // we can add it locally if necessary
    471             } else {
    472                 File oldPath = new File(mItem.mFilePath);
    473                 File directory = new File(oldPath.getParent());
    474 
    475                 int x = 0;
    476                 String fileName = oldPath.getName();
    477                 fileName = fileName.substring(0, fileName.lastIndexOf("."));
    478 
    479                 // Try file-1.jpg, file-2.jpg, ... until we find a filename
    480                 // which
    481                 // does not exist yet.
    482                 while (true) {
    483                     x += 1;
    484                     String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg";
    485                     boolean exists = (new File(candidate)).exists();
    486                     if (!exists) { // CR: inline the expression for exists
    487                                    // here--it's clear enough.
    488                         break;
    489                     }
    490                 }
    491 
    492                 MediaItem item = mItem;
    493                 String title = fileName + "-" + x;
    494                 String finalFileName = title + ".jpg";
    495                 int[] degree = new int[1];
    496                 Double latitude = null;
    497                 Double longitude = null;
    498                 if (item.isLatLongValid()) {
    499                     latitude = new Double(item.mLatitude);
    500                     longitude = new Double(item.mLongitude);
    501                 }
    502                 Uri newUri = ImageManager.addImage(mContentResolver, title,
    503                         item.mDateAddedInSec, item.mDateTakenInMs, latitude,
    504                         longitude, directory.toString(), finalFileName,
    505                         croppedImage, null, degree);
    506                 if (newUri != null) {
    507                     setResult(RESULT_OK, new Intent().setAction(newUri.toString()).putExtras(extras));
    508                 } else {
    509                     setResult(RESULT_OK, new Intent().setAction(null));
    510                 }
    511             }
    512         }
    513         croppedImage.recycle();
    514         finish();
    515     }
    516 
    517     @Override
    518     protected void onResume() {
    519         super.onResume();
    520     	mApp.onResume();
    521     }
    522 
    523     @Override
    524     protected void onPause() {
    525         super.onPause();
    526         BitmapManager.instance().cancelThreadDecoding(mDecodingThreads);
    527     	mApp.onPause();
    528     }
    529 
    530     @Override
    531     protected void onDestroy() {
    532         mApp.shutdown();
    533         super.onDestroy();
    534     }
    535 
    536     Runnable mRunFaceDetection = new Runnable() {
    537         float mScale = 1F;
    538         Matrix mImageMatrix;
    539         FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
    540         int mNumFaces;
    541 
    542         // For each face, we create a HightlightView for it.
    543         private void handleFace(FaceDetector.Face f) {
    544             PointF midPoint = new PointF();
    545 
    546             int r = ((int) (f.eyesDistance() * mScale)) * 2;
    547             f.getMidPoint(midPoint);
    548             midPoint.x *= mScale;
    549             midPoint.y *= mScale;
    550 
    551             int midX = (int) midPoint.x;
    552             int midY = (int) midPoint.y;
    553 
    554             HighlightView hv = new HighlightView(mImageView);
    555 
    556             int width = mBitmap.getWidth();
    557             int height = mBitmap.getHeight();
    558 
    559             Rect imageRect = new Rect(0, 0, width, height);
    560 
    561             RectF faceRect = new RectF(midX, midY, midX, midY);
    562             faceRect.inset(-r, -r);
    563             if (faceRect.left < 0) {
    564                 faceRect.inset(-faceRect.left, -faceRect.left);
    565             }
    566 
    567             if (faceRect.top < 0) {
    568                 faceRect.inset(-faceRect.top, -faceRect.top);
    569             }
    570 
    571             if (faceRect.right > imageRect.right) {
    572                 faceRect.inset(faceRect.right - imageRect.right, faceRect.right - imageRect.right);
    573             }
    574 
    575             if (faceRect.bottom > imageRect.bottom) {
    576                 faceRect.inset(faceRect.bottom - imageRect.bottom, faceRect.bottom - imageRect.bottom);
    577             }
    578 
    579             hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
    580 
    581             mImageView.add(hv);
    582         }
    583 
    584         // Create a default HightlightView if we found no face in the picture.
    585         private void makeDefault() {
    586             HighlightView hv = new HighlightView(mImageView);
    587 
    588             int width = mBitmap.getWidth();
    589             int height = mBitmap.getHeight();
    590 
    591             Rect imageRect = new Rect(0, 0, width, height);
    592 
    593             // CR: sentences!
    594             // make the default size about 4/5 of the width or height
    595             int cropWidth = Math.min(width, height) * 4 / 5;
    596             int cropHeight = cropWidth;
    597 
    598             if (mAspectX != 0 && mAspectY != 0) {
    599                 if (mAspectX > mAspectY) {
    600                     cropHeight = cropWidth * mAspectY / mAspectX;
    601                 } else {
    602                     cropWidth = cropHeight * mAspectX / mAspectY;
    603                 }
    604             }
    605 
    606             int x = (width - cropWidth) / 2;
    607             int y = (height - cropHeight) / 2;
    608 
    609             RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
    610             hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop, mAspectX != 0 && mAspectY != 0);
    611             mImageView.add(hv);
    612         }
    613 
    614         // Scale the image down for faster face detection.
    615         private Bitmap prepareBitmap() {
    616             if (mBitmap == null) {
    617                 return null;
    618             }
    619 
    620             // 256 pixels wide is enough.
    621             if (mBitmap.getWidth() > 256) {
    622                 mScale = 256.0F / mBitmap.getWidth(); // CR: F => f (or change
    623                                                       // all f to F).
    624             }
    625             Matrix matrix = new Matrix();
    626             matrix.setScale(mScale, mScale);
    627             Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true);
    628             return faceBitmap;
    629         }
    630 
    631         public void run() {
    632             mImageMatrix = mImageView.getImageMatrix();
    633             Bitmap faceBitmap = prepareBitmap();
    634 
    635             mScale = 1.0F / mScale;
    636             if (faceBitmap != null && mDoFaceDetection) {
    637                 FaceDetector detector = new FaceDetector(faceBitmap.getWidth(), faceBitmap.getHeight(), mFaces.length);
    638                 mNumFaces = detector.findFaces(faceBitmap, mFaces);
    639             }
    640 
    641             if (faceBitmap != null && faceBitmap != mBitmap) {
    642                 faceBitmap.recycle();
    643             }
    644 
    645             mHandler.post(new Runnable() {
    646                 public void run() {
    647                     mWaitingToPick = mNumFaces > 1;
    648                     if (mNumFaces > 0) {
    649                         for (int i = 0; i < mNumFaces; i++) {
    650                             handleFace(mFaces[i]);
    651                         }
    652                     } else {
    653                         makeDefault();
    654                     }
    655                     mImageView.invalidate();
    656                     if (mImageView.mHighlightViews.size() == 1) {
    657                         mCrop = mImageView.mHighlightViews.get(0);
    658                         mCrop.setFocus(true);
    659                     }
    660 
    661                     if (mNumFaces > 1) {
    662                         // CR: no need for the variable t. just do
    663                         // Toast.makeText(...).show().
    664                         Toast t = Toast.makeText(CropImage.this, Res.string.multiface_crop_help, Toast.LENGTH_SHORT);
    665                         t.show();
    666                     }
    667                 }
    668             });
    669         }
    670     };
    671 }
    672 
    673 class CropImageView extends ImageViewTouchBase {
    674     ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
    675     HighlightView mMotionHighlightView = null;
    676     float mLastX, mLastY;
    677     int mMotionEdge;
    678 
    679     @Override
    680     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    681         super.onLayout(changed, left, top, right, bottom);
    682         if (mBitmapDisplayed.getBitmap() != null) {
    683             for (HighlightView hv : mHighlightViews) {
    684                 hv.mMatrix.set(getImageMatrix());
    685                 hv.invalidate();
    686                 if (hv.mIsFocused) {
    687                     centerBasedOnHighlightView(hv);
    688                 }
    689             }
    690         }
    691     }
    692 
    693     public CropImageView(Context context, AttributeSet attrs) {
    694         super(context, attrs);
    695     }
    696 
    697     @Override
    698     protected void zoomTo(float scale, float centerX, float centerY) {
    699         super.zoomTo(scale, centerX, centerY);
    700         for (HighlightView hv : mHighlightViews) {
    701             hv.mMatrix.set(getImageMatrix());
    702             hv.invalidate();
    703         }
    704     }
    705 
    706     @Override
    707     protected void zoomIn() {
    708         super.zoomIn();
    709         for (HighlightView hv : mHighlightViews) {
    710             hv.mMatrix.set(getImageMatrix());
    711             hv.invalidate();
    712         }
    713     }
    714 
    715     @Override
    716     protected void zoomOut() {
    717         super.zoomOut();
    718         for (HighlightView hv : mHighlightViews) {
    719             hv.mMatrix.set(getImageMatrix());
    720             hv.invalidate();
    721         }
    722     }
    723 
    724     @Override
    725     protected void postTranslate(float deltaX, float deltaY) {
    726         super.postTranslate(deltaX, deltaY);
    727         for (int i = 0; i < mHighlightViews.size(); i++) {
    728             HighlightView hv = mHighlightViews.get(i);
    729             hv.mMatrix.postTranslate(deltaX, deltaY);
    730             hv.invalidate();
    731         }
    732     }
    733 
    734     // According to the event's position, change the focus to the first
    735     // hitting cropping rectangle.
    736     private void recomputeFocus(MotionEvent event) {
    737         for (int i = 0; i < mHighlightViews.size(); i++) {
    738             HighlightView hv = mHighlightViews.get(i);
    739             hv.setFocus(false);
    740             hv.invalidate();
    741         }
    742 
    743         for (int i = 0; i < mHighlightViews.size(); i++) {
    744             HighlightView hv = mHighlightViews.get(i);
    745             int edge = hv.getHit(event.getX(), event.getY());
    746             if (edge != HighlightView.GROW_NONE) {
    747                 if (!hv.hasFocus()) {
    748                     hv.setFocus(true);
    749                     hv.invalidate();
    750                 }
    751                 break;
    752             }
    753         }
    754         invalidate();
    755     }
    756 
    757     @Override
    758     public boolean onTouchEvent(MotionEvent event) {
    759         CropImage cropImage = (CropImage) getContext();
    760         if (cropImage.mSaving) {
    761             return false;
    762         }
    763 
    764         switch (event.getAction()) {
    765         case MotionEvent.ACTION_DOWN: // CR: inline case blocks.
    766             if (cropImage.mWaitingToPick) {
    767                 recomputeFocus(event);
    768             } else {
    769                 for (int i = 0; i < mHighlightViews.size(); i++) { // CR:
    770                                                                    // iterator
    771                                                                    // for; if
    772                                                                    // not, then
    773                                                                    // i++ =>
    774                                                                    // ++i.
    775                     HighlightView hv = mHighlightViews.get(i);
    776                     int edge = hv.getHit(event.getX(), event.getY());
    777                     if (edge != HighlightView.GROW_NONE) {
    778                         mMotionEdge = edge;
    779                         mMotionHighlightView = hv;
    780                         mLastX = event.getX();
    781                         mLastY = event.getY();
    782                         // CR: get rid of the extraneous parens below.
    783                         mMotionHighlightView.setMode((edge == HighlightView.MOVE) ? HighlightView.ModifyMode.Move
    784                                 : HighlightView.ModifyMode.Grow);
    785                         break;
    786                     }
    787                 }
    788             }
    789             break;
    790         // CR: vertical space before case blocks.
    791         case MotionEvent.ACTION_UP:
    792             if (cropImage.mWaitingToPick) {
    793                 for (int i = 0; i < mHighlightViews.size(); i++) {
    794                     HighlightView hv = mHighlightViews.get(i);
    795                     if (hv.hasFocus()) {
    796                         cropImage.mCrop = hv;
    797                         for (int j = 0; j < mHighlightViews.size(); j++) {
    798                             if (j == i) { // CR: if j != i do your shit; no need
    799                                           // for continue.
    800                                 continue;
    801                             }
    802                             mHighlightViews.get(j).setHidden(true);
    803                         }
    804                         centerBasedOnHighlightView(hv);
    805                         ((CropImage) getContext()).mWaitingToPick = false;
    806                         return true;
    807                     }
    808                 }
    809             } else if (mMotionHighlightView != null) {
    810                 centerBasedOnHighlightView(mMotionHighlightView);
    811                 mMotionHighlightView.setMode(HighlightView.ModifyMode.None);
    812             }
    813             mMotionHighlightView = null;
    814             break;
    815         case MotionEvent.ACTION_MOVE:
    816             if (cropImage.mWaitingToPick) {
    817                 recomputeFocus(event);
    818             } else if (mMotionHighlightView != null) {
    819                 mMotionHighlightView.handleMotion(mMotionEdge, event.getX() - mLastX, event.getY() - mLastY);
    820                 mLastX = event.getX();
    821                 mLastY = event.getY();
    822 
    823                 if (true) {
    824                     // This section of code is optional. It has some user
    825                     // benefit in that moving the crop rectangle against
    826                     // the edge of the screen causes scrolling but it means
    827                     // that the crop rectangle is no longer fixed under
    828                     // the user's finger.
    829                     ensureVisible(mMotionHighlightView);
    830                 }
    831             }
    832             break;
    833         }
    834 
    835         switch (event.getAction()) {
    836         case MotionEvent.ACTION_UP:
    837             center(true, true);
    838             break;
    839         case MotionEvent.ACTION_MOVE:
    840             // if we're not zoomed then there's no point in even allowing
    841             // the user to move the image around. This call to center puts
    842             // it back to the normalized location (with false meaning don't
    843             // animate).
    844             if (getScale() == 1F) {
    845                 center(true, true);
    846             }
    847             break;
    848         }
    849 
    850         return true;
    851     }
    852 
    853     // Pan the displayed image to make sure the cropping rectangle is visible.
    854     private void ensureVisible(HighlightView hv) {
    855         Rect r = hv.mDrawRect;
    856 
    857         int panDeltaX1 = Math.max(0, getLeft() - r.left);
    858         int panDeltaX2 = Math.min(0, getRight() - r.right);
    859 
    860         int panDeltaY1 = Math.max(0, getTop() - r.top);
    861         int panDeltaY2 = Math.min(0, getBottom() - r.bottom);
    862 
    863         int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
    864         int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
    865 
    866         if (panDeltaX != 0 || panDeltaY != 0) {
    867             panBy(panDeltaX, panDeltaY);
    868         }
    869     }
    870 
    871     // If the cropping rectangle's size changed significantly, change the
    872     // view's center and scale according to the cropping rectangle.
    873     private void centerBasedOnHighlightView(HighlightView hv) {
    874         Rect drawRect = hv.mDrawRect;
    875 
    876         float width = drawRect.width();
    877         float height = drawRect.height();
    878 
    879         float thisWidth = getWidth();
    880         float thisHeight = getHeight();
    881 
    882         float z1 = thisWidth / width * .6F;
    883         float z2 = thisHeight / height * .6F;
    884 
    885         float zoom = Math.min(z1, z2);
    886         zoom = zoom * this.getScale();
    887         zoom = Math.max(1F, zoom);
    888 
    889         if ((Math.abs(zoom - getScale()) / zoom) > .1) {
    890             float[] coordinates = new float[] { hv.mCropRect.centerX(), hv.mCropRect.centerY() };
    891             getImageMatrix().mapPoints(coordinates);
    892             zoomTo(zoom, coordinates[0], coordinates[1], 300F); // CR: 300.0f.
    893         }
    894 
    895         ensureVisible(hv);
    896     }
    897 
    898     @Override
    899     protected void onDraw(Canvas canvas) {
    900         super.onDraw(canvas);
    901         for (int i = 0; i < mHighlightViews.size(); i++) {
    902             mHighlightViews.get(i).draw(canvas);
    903         }
    904     }
    905 
    906     public void add(HighlightView hv) {
    907         mHighlightViews.add(hv);
    908         invalidate();
    909     }
    910 }
    911