1/*
2 * Copyright (C) 2010 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
17package com.android.gallery3d.app;
18
19import android.app.ActionBar;
20import android.app.ProgressDialog;
21import android.app.WallpaperManager;
22import android.content.ContentValues;
23import android.content.Intent;
24import android.graphics.Bitmap;
25import android.graphics.Bitmap.CompressFormat;
26import android.graphics.Bitmap.Config;
27import android.graphics.BitmapFactory;
28import android.graphics.BitmapRegionDecoder;
29import android.graphics.Canvas;
30import android.graphics.Paint;
31import android.graphics.Rect;
32import android.graphics.RectF;
33import android.media.ExifInterface;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.Environment;
37import android.os.Handler;
38import android.os.Message;
39import android.provider.MediaStore;
40import android.provider.MediaStore.Images;
41import android.view.Menu;
42import android.view.MenuItem;
43import android.view.Window;
44import android.widget.Toast;
45
46import com.android.gallery3d.R;
47import com.android.gallery3d.common.BitmapUtils;
48import com.android.gallery3d.common.Utils;
49import com.android.gallery3d.data.DataManager;
50import com.android.gallery3d.data.LocalImage;
51import com.android.gallery3d.data.MediaItem;
52import com.android.gallery3d.data.MediaObject;
53import com.android.gallery3d.data.Path;
54import com.android.gallery3d.picasasource.PicasaSource;
55import com.android.gallery3d.ui.BitmapTileProvider;
56import com.android.gallery3d.ui.CropView;
57import com.android.gallery3d.ui.GLRoot;
58import com.android.gallery3d.ui.SynchronizedHandler;
59import com.android.gallery3d.ui.TileImageViewAdapter;
60import com.android.gallery3d.util.Future;
61import com.android.gallery3d.util.FutureListener;
62import com.android.gallery3d.util.GalleryUtils;
63import com.android.gallery3d.util.InterruptableOutputStream;
64import com.android.gallery3d.util.ThreadPool.CancelListener;
65import com.android.gallery3d.util.ThreadPool.Job;
66import com.android.gallery3d.util.ThreadPool.JobContext;
67
68import java.io.File;
69import java.io.FileNotFoundException;
70import java.io.FileOutputStream;
71import java.io.IOException;
72import java.io.OutputStream;
73import java.text.SimpleDateFormat;
74import java.util.Date;
75
76/**
77 * The activity can crop specific region of interest from an image.
78 */
79public class CropImage extends AbstractGalleryActivity {
80    private static final String TAG = "CropImage";
81    public static final String ACTION_CROP = "com.android.camera.action.CROP";
82
83    private static final int MAX_PIXEL_COUNT = 5 * 1000000; // 5M pixels
84    private static final int MAX_FILE_INDEX = 1000;
85    private static final int TILE_SIZE = 512;
86    private static final int BACKUP_PIXEL_COUNT = 480000; // around 800x600
87
88    private static final int MSG_LARGE_BITMAP = 1;
89    private static final int MSG_BITMAP = 2;
90    private static final int MSG_SAVE_COMPLETE = 3;
91    private static final int MSG_SHOW_SAVE_ERROR = 4;
92
93    private static final int MAX_BACKUP_IMAGE_SIZE = 320;
94    private static final int DEFAULT_COMPRESS_QUALITY = 90;
95    private static final String TIME_STAMP_NAME = "'IMG'_yyyyMMdd_HHmmss";
96
97    // Change these to Images.Media.WIDTH/HEIGHT after they are unhidden.
98    private static final String WIDTH = "width";
99    private static final String HEIGHT = "height";
100
101    public static final String KEY_RETURN_DATA = "return-data";
102    public static final String KEY_CROPPED_RECT = "cropped-rect";
103    public static final String KEY_ASPECT_X = "aspectX";
104    public static final String KEY_ASPECT_Y = "aspectY";
105    public static final String KEY_SPOTLIGHT_X = "spotlightX";
106    public static final String KEY_SPOTLIGHT_Y = "spotlightY";
107    public static final String KEY_OUTPUT_X = "outputX";
108    public static final String KEY_OUTPUT_Y = "outputY";
109    public static final String KEY_SCALE = "scale";
110    public static final String KEY_DATA = "data";
111    public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
112    public static final String KEY_OUTPUT_FORMAT = "outputFormat";
113    public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
114    public static final String KEY_NO_FACE_DETECTION = "noFaceDetection";
115
116    private static final String KEY_STATE = "state";
117
118    private static final int STATE_INIT = 0;
119    private static final int STATE_LOADED = 1;
120    private static final int STATE_SAVING = 2;
121
122    public static final String DOWNLOAD_STRING = "download";
123    public static final File DOWNLOAD_BUCKET = new File(
124            Environment.getExternalStorageDirectory(), DOWNLOAD_STRING);
125
126    public static final String CROP_ACTION = "com.android.camera.action.CROP";
127
128    private int mState = STATE_INIT;
129
130    private CropView mCropView;
131
132    private boolean mDoFaceDetection = true;
133
134    private Handler mMainHandler;
135
136    // We keep the following members so that we can free them
137
138    // mBitmap is the unrotated bitmap we pass in to mCropView for detect faces.
139    // mCropView is responsible for rotating it to the way that it is viewed by users.
140    private Bitmap mBitmap;
141    private BitmapTileProvider mBitmapTileProvider;
142    private BitmapRegionDecoder mRegionDecoder;
143    private Bitmap mBitmapInIntent;
144    private boolean mUseRegionDecoder = false;
145
146    private ProgressDialog mProgressDialog;
147    private Future<BitmapRegionDecoder> mLoadTask;
148    private Future<Bitmap> mLoadBitmapTask;
149    private Future<Intent> mSaveTask;
150
151    private MediaItem mMediaItem;
152
153    @Override
154    public void onCreate(Bundle bundle) {
155        super.onCreate(bundle);
156        requestWindowFeature(Window.FEATURE_ACTION_BAR);
157        requestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY);
158
159        // Initialize UI
160        setContentView(R.layout.cropimage);
161        mCropView = new CropView(this);
162        getGLRoot().setContentPane(mCropView);
163
164        ActionBar actionBar = getActionBar();
165        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP,
166                ActionBar.DISPLAY_HOME_AS_UP);
167
168        mMainHandler = new SynchronizedHandler(getGLRoot()) {
169            @Override
170            public void handleMessage(Message message) {
171                switch (message.what) {
172                    case MSG_LARGE_BITMAP: {
173                        mProgressDialog.dismiss();
174                        onBitmapRegionDecoderAvailable((BitmapRegionDecoder) message.obj);
175                        break;
176                    }
177                    case MSG_BITMAP: {
178                        mProgressDialog.dismiss();
179                        onBitmapAvailable((Bitmap) message.obj);
180                        break;
181                    }
182                    case MSG_SHOW_SAVE_ERROR: {
183                        mProgressDialog.dismiss();
184                        setResult(RESULT_CANCELED);
185                        Toast.makeText(CropImage.this,
186                                CropImage.this.getString(R.string.save_error),
187                                Toast.LENGTH_LONG).show();
188                        finish();
189                    }
190                    case MSG_SAVE_COMPLETE: {
191                        mProgressDialog.dismiss();
192                        setResult(RESULT_OK, (Intent) message.obj);
193                        finish();
194                        break;
195                    }
196                }
197            }
198        };
199
200        setCropParameters();
201    }
202
203    @Override
204    protected void onSaveInstanceState(Bundle saveState) {
205        saveState.putInt(KEY_STATE, mState);
206    }
207
208    @Override
209    public boolean onCreateOptionsMenu(Menu menu) {
210        super.onCreateOptionsMenu(menu);
211        getMenuInflater().inflate(R.menu.crop, menu);
212        return true;
213    }
214
215    @Override
216    public boolean onOptionsItemSelected(MenuItem item) {
217        switch (item.getItemId()) {
218            case android.R.id.home: {
219                finish();
220                break;
221            }
222            case R.id.cancel: {
223                setResult(RESULT_CANCELED);
224                finish();
225                break;
226            }
227            case R.id.save: {
228                onSaveClicked();
229                break;
230            }
231        }
232        return true;
233    }
234
235    private class SaveOutput implements Job<Intent> {
236        private final RectF mCropRect;
237
238        public SaveOutput(RectF cropRect) {
239            mCropRect = cropRect;
240        }
241
242        public Intent run(JobContext jc) {
243            RectF cropRect = mCropRect;
244            Bundle extra = getIntent().getExtras();
245
246            Rect rect = new Rect(
247                    Math.round(cropRect.left), Math.round(cropRect.top),
248                    Math.round(cropRect.right), Math.round(cropRect.bottom));
249
250            Intent result = new Intent();
251            result.putExtra(KEY_CROPPED_RECT, rect);
252            Bitmap cropped = null;
253            boolean outputted = false;
254            if (extra != null) {
255                Uri uri = (Uri) extra.getParcelable(MediaStore.EXTRA_OUTPUT);
256                if (uri != null) {
257                    if (jc.isCancelled()) return null;
258                    outputted = true;
259                    cropped = getCroppedImage(rect);
260                    if (!saveBitmapToUri(jc, cropped, uri)) return null;
261                }
262                if (extra.getBoolean(KEY_RETURN_DATA, false)) {
263                    if (jc.isCancelled()) return null;
264                    outputted = true;
265                    if (cropped == null) cropped = getCroppedImage(rect);
266                    result.putExtra(KEY_DATA, cropped);
267                }
268                if (extra.getBoolean(KEY_SET_AS_WALLPAPER, false)) {
269                    if (jc.isCancelled()) return null;
270                    outputted = true;
271                    if (cropped == null) cropped = getCroppedImage(rect);
272                    if (!setAsWallpaper(jc, cropped)) return null;
273                }
274            }
275            if (!outputted) {
276                if (jc.isCancelled()) return null;
277                if (cropped == null) cropped = getCroppedImage(rect);
278                Uri data = saveToMediaProvider(jc, cropped);
279                if (data != null) result.setData(data);
280            }
281            return result;
282        }
283    }
284
285    public static String determineCompressFormat(MediaObject obj) {
286        String compressFormat = "JPEG";
287        if (obj instanceof MediaItem) {
288            String mime = ((MediaItem) obj).getMimeType();
289            if (mime.contains("png") || mime.contains("gif")) {
290              // Set the compress format to PNG for png and gif images
291              // because they may contain alpha values.
292              compressFormat = "PNG";
293            }
294        }
295        return compressFormat;
296    }
297
298    private boolean setAsWallpaper(JobContext jc, Bitmap wallpaper) {
299        try {
300            WallpaperManager.getInstance(this).setBitmap(wallpaper);
301        } catch (IOException e) {
302            Log.w(TAG, "fail to set wall paper", e);
303        }
304        return true;
305    }
306
307    private File saveMedia(
308            JobContext jc, Bitmap cropped, File directory, String filename) {
309        // Try file-1.jpg, file-2.jpg, ... until we find a filename
310        // which does not exist yet.
311        File candidate = null;
312        String fileExtension = getFileExtension();
313        for (int i = 1; i < MAX_FILE_INDEX; ++i) {
314            candidate = new File(directory, filename + "-" + i + "."
315                    + fileExtension);
316            try {
317                if (candidate.createNewFile()) break;
318            } catch (IOException e) {
319                Log.e(TAG, "fail to create new file: "
320                        + candidate.getAbsolutePath(), e);
321                return null;
322            }
323        }
324        if (!candidate.exists() || !candidate.isFile()) {
325            throw new RuntimeException("cannot create file: " + filename);
326        }
327
328        candidate.setReadable(true, false);
329        candidate.setWritable(true, false);
330
331        try {
332            FileOutputStream fos = new FileOutputStream(candidate);
333            try {
334                saveBitmapToOutputStream(jc, cropped,
335                        convertExtensionToCompressFormat(fileExtension), fos);
336            } finally {
337                fos.close();
338            }
339        } catch (IOException e) {
340            Log.e(TAG, "fail to save image: "
341                    + candidate.getAbsolutePath(), e);
342            candidate.delete();
343            return null;
344        }
345
346        if (jc.isCancelled()) {
347            candidate.delete();
348            return null;
349        }
350
351        return candidate;
352    }
353
354    private Uri saveToMediaProvider(JobContext jc, Bitmap cropped) {
355        if (PicasaSource.isPicasaImage(mMediaItem)) {
356            return savePicasaImage(jc, cropped);
357        } else if (mMediaItem instanceof LocalImage) {
358            return saveLocalImage(jc, cropped);
359        } else {
360            return saveGenericImage(jc, cropped);
361        }
362    }
363
364    private Uri savePicasaImage(JobContext jc, Bitmap cropped) {
365        if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
366            throw new RuntimeException("cannot create download folder");
367        }
368
369        String filename = PicasaSource.getImageTitle(mMediaItem);
370        int pos = filename.lastIndexOf('.');
371        if (pos >= 0) filename = filename.substring(0, pos);
372        File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
373        if (output == null) return null;
374
375        copyExif(mMediaItem, output.getAbsolutePath(), cropped.getWidth(), cropped.getHeight());
376
377        long now = System.currentTimeMillis() / 1000;
378        ContentValues values = new ContentValues();
379        values.put(Images.Media.TITLE, PicasaSource.getImageTitle(mMediaItem));
380        values.put(Images.Media.DISPLAY_NAME, output.getName());
381        values.put(Images.Media.DATE_TAKEN, PicasaSource.getDateTaken(mMediaItem));
382        values.put(Images.Media.DATE_MODIFIED, now);
383        values.put(Images.Media.DATE_ADDED, now);
384        values.put(Images.Media.MIME_TYPE, getOutputMimeType());
385        values.put(Images.Media.ORIENTATION, 0);
386        values.put(Images.Media.DATA, output.getAbsolutePath());
387        values.put(Images.Media.SIZE, output.length());
388        values.put(WIDTH, cropped.getWidth());
389        values.put(HEIGHT, cropped.getHeight());
390
391        double latitude = PicasaSource.getLatitude(mMediaItem);
392        double longitude = PicasaSource.getLongitude(mMediaItem);
393        if (GalleryUtils.isValidLocation(latitude, longitude)) {
394            values.put(Images.Media.LATITUDE, latitude);
395            values.put(Images.Media.LONGITUDE, longitude);
396        }
397        return getContentResolver().insert(
398                Images.Media.EXTERNAL_CONTENT_URI, values);
399    }
400
401    private Uri saveLocalImage(JobContext jc, Bitmap cropped) {
402        LocalImage localImage = (LocalImage) mMediaItem;
403
404        File oldPath = new File(localImage.filePath);
405        File directory = new File(oldPath.getParent());
406
407        String filename = oldPath.getName();
408        int pos = filename.lastIndexOf('.');
409        if (pos >= 0) filename = filename.substring(0, pos);
410        File output = saveMedia(jc, cropped, directory, filename);
411        if (output == null) return null;
412
413        copyExif(oldPath.getAbsolutePath(), output.getAbsolutePath(),
414                cropped.getWidth(), cropped.getHeight());
415
416        long now = System.currentTimeMillis() / 1000;
417        ContentValues values = new ContentValues();
418        values.put(Images.Media.TITLE, localImage.caption);
419        values.put(Images.Media.DISPLAY_NAME, output.getName());
420        values.put(Images.Media.DATE_TAKEN, localImage.dateTakenInMs);
421        values.put(Images.Media.DATE_MODIFIED, now);
422        values.put(Images.Media.DATE_ADDED, now);
423        values.put(Images.Media.MIME_TYPE, getOutputMimeType());
424        values.put(Images.Media.ORIENTATION, 0);
425        values.put(Images.Media.DATA, output.getAbsolutePath());
426        values.put(Images.Media.SIZE, output.length());
427        values.put(WIDTH, cropped.getWidth());
428        values.put(HEIGHT, cropped.getHeight());
429
430        if (GalleryUtils.isValidLocation(localImage.latitude, localImage.longitude)) {
431            values.put(Images.Media.LATITUDE, localImage.latitude);
432            values.put(Images.Media.LONGITUDE, localImage.longitude);
433        }
434        return getContentResolver().insert(
435                Images.Media.EXTERNAL_CONTENT_URI, values);
436    }
437
438    private Uri saveGenericImage(JobContext jc, Bitmap cropped) {
439        if (!DOWNLOAD_BUCKET.isDirectory() && !DOWNLOAD_BUCKET.mkdirs()) {
440            throw new RuntimeException("cannot create download folder");
441        }
442
443        long now = System.currentTimeMillis();
444        String filename = new SimpleDateFormat(TIME_STAMP_NAME).
445                format(new Date(now));
446
447        File output = saveMedia(jc, cropped, DOWNLOAD_BUCKET, filename);
448        if (output == null) return null;
449
450        ContentValues values = new ContentValues();
451        values.put(Images.Media.TITLE, filename);
452        values.put(Images.Media.DISPLAY_NAME, output.getName());
453        values.put(Images.Media.DATE_TAKEN, now);
454        values.put(Images.Media.DATE_MODIFIED, now / 1000);
455        values.put(Images.Media.DATE_ADDED, now / 1000);
456        values.put(Images.Media.MIME_TYPE, getOutputMimeType());
457        values.put(Images.Media.ORIENTATION, 0);
458        values.put(Images.Media.DATA, output.getAbsolutePath());
459        values.put(Images.Media.SIZE, output.length());
460        values.put(WIDTH, cropped.getWidth());
461        values.put(HEIGHT, cropped.getHeight());
462
463        return getContentResolver().insert(
464                Images.Media.EXTERNAL_CONTENT_URI, values);
465    }
466
467    private boolean saveBitmapToOutputStream(
468            JobContext jc, Bitmap bitmap, CompressFormat format, OutputStream os) {
469        // We wrap the OutputStream so that it can be interrupted.
470        final InterruptableOutputStream ios = new InterruptableOutputStream(os);
471        jc.setCancelListener(new CancelListener() {
472                public void onCancel() {
473                    ios.interrupt();
474                }
475            });
476        try {
477            bitmap.compress(format, DEFAULT_COMPRESS_QUALITY, os);
478            return !jc.isCancelled();
479        } finally {
480            jc.setCancelListener(null);
481            Utils.closeSilently(os);
482        }
483    }
484
485    private boolean saveBitmapToUri(JobContext jc, Bitmap bitmap, Uri uri) {
486        try {
487            return saveBitmapToOutputStream(jc, bitmap,
488                    convertExtensionToCompressFormat(getFileExtension()),
489                    getContentResolver().openOutputStream(uri));
490        } catch (FileNotFoundException e) {
491            Log.w(TAG, "cannot write output", e);
492        }
493        return true;
494    }
495
496    private CompressFormat convertExtensionToCompressFormat(String extension) {
497        return extension.equals("png")
498                ? CompressFormat.PNG
499                : CompressFormat.JPEG;
500    }
501
502    private String getOutputMimeType() {
503        return getFileExtension().equals("png") ? "image/png" : "image/jpeg";
504    }
505
506    private String getFileExtension() {
507        String requestFormat = getIntent().getStringExtra(KEY_OUTPUT_FORMAT);
508        String outputFormat = (requestFormat == null)
509                ? determineCompressFormat(mMediaItem)
510                : requestFormat;
511
512        outputFormat = outputFormat.toLowerCase();
513        return (outputFormat.equals("png") || outputFormat.equals("gif"))
514                ? "png" // We don't support gif compression.
515                : "jpg";
516    }
517
518    private void onSaveClicked() {
519        Bundle extra = getIntent().getExtras();
520        RectF cropRect = mCropView.getCropRectangle();
521        if (cropRect == null) return;
522        mState = STATE_SAVING;
523        int messageId = extra != null && extra.getBoolean(KEY_SET_AS_WALLPAPER)
524                ? R.string.wallpaper
525                : R.string.saving_image;
526        mProgressDialog = ProgressDialog.show(
527                this, null, getString(messageId), true, false);
528        mSaveTask = getThreadPool().submit(new SaveOutput(cropRect),
529                new FutureListener<Intent>() {
530            public void onFutureDone(Future<Intent> future) {
531                mSaveTask = null;
532                if (future.isCancelled()) return;
533                Intent intent = future.get();
534                if (intent != null) {
535                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
536                            MSG_SAVE_COMPLETE, intent));
537                } else {
538                    mMainHandler.sendEmptyMessage(MSG_SHOW_SAVE_ERROR);
539                }
540            }
541        });
542    }
543
544    private Bitmap getCroppedImage(Rect rect) {
545        Utils.assertTrue(rect.width() > 0 && rect.height() > 0);
546
547        Bundle extras = getIntent().getExtras();
548        // (outputX, outputY) = the width and height of the returning bitmap.
549        int outputX = rect.width();
550        int outputY = rect.height();
551        if (extras != null) {
552            outputX = extras.getInt(KEY_OUTPUT_X, outputX);
553            outputY = extras.getInt(KEY_OUTPUT_Y, outputY);
554        }
555
556        if (outputX * outputY > MAX_PIXEL_COUNT) {
557            float scale = (float) Math.sqrt(
558                    (double) MAX_PIXEL_COUNT / outputX / outputY);
559            Log.w(TAG, "scale down the cropped image: " + scale);
560            outputX = Math.round(scale * outputX);
561            outputY = Math.round(scale * outputY);
562        }
563
564        // (rect.width() * scaleX, rect.height() * scaleY) =
565        // the size of drawing area in output bitmap
566        float scaleX = 1;
567        float scaleY = 1;
568        Rect dest = new Rect(0, 0, outputX, outputY);
569        if (extras == null || extras.getBoolean(KEY_SCALE, true)) {
570            scaleX = (float) outputX / rect.width();
571            scaleY = (float) outputY / rect.height();
572            if (extras == null || !extras.getBoolean(
573                    KEY_SCALE_UP_IF_NEEDED, false)) {
574                if (scaleX > 1f) scaleX = 1;
575                if (scaleY > 1f) scaleY = 1;
576            }
577        }
578
579        // Keep the content in the center (or crop the content)
580        int rectWidth = Math.round(rect.width() * scaleX);
581        int rectHeight = Math.round(rect.height() * scaleY);
582        dest.set(Math.round((outputX - rectWidth) / 2f),
583                Math.round((outputY - rectHeight) / 2f),
584                Math.round((outputX + rectWidth) / 2f),
585                Math.round((outputY + rectHeight) / 2f));
586
587        if (mBitmapInIntent != null) {
588            Bitmap source = mBitmapInIntent;
589            Bitmap result = Bitmap.createBitmap(
590                    outputX, outputY, Config.ARGB_8888);
591            Canvas canvas = new Canvas(result);
592            canvas.drawBitmap(source, rect, dest, null);
593            return result;
594        }
595
596        if (mUseRegionDecoder) {
597            int rotation = mMediaItem.getFullImageRotation();
598            rotateRectangle(rect, mCropView.getImageWidth(),
599                    mCropView.getImageHeight(), 360 - rotation);
600            rotateRectangle(dest, outputX, outputY, 360 - rotation);
601
602            BitmapFactory.Options options = new BitmapFactory.Options();
603            int sample = BitmapUtils.computeSampleSizeLarger(
604                    Math.max(scaleX, scaleY));
605            options.inSampleSize = sample;
606            if ((rect.width() / sample) == dest.width()
607                    && (rect.height() / sample) == dest.height()
608                    && rotation == 0) {
609                // To prevent concurrent access in GLThread
610                synchronized (mRegionDecoder) {
611                    return mRegionDecoder.decodeRegion(rect, options);
612                }
613            }
614            Bitmap result = Bitmap.createBitmap(
615                    outputX, outputY, Config.ARGB_8888);
616            Canvas canvas = new Canvas(result);
617            rotateCanvas(canvas, outputX, outputY, rotation);
618            drawInTiles(canvas, mRegionDecoder, rect, dest, sample);
619            return result;
620        } else {
621            int rotation = mMediaItem.getRotation();
622            rotateRectangle(rect, mCropView.getImageWidth(),
623                    mCropView.getImageHeight(), 360 - rotation);
624            rotateRectangle(dest, outputX, outputY, 360 - rotation);
625            Bitmap result = Bitmap.createBitmap(outputX, outputY, Config.ARGB_8888);
626            Canvas canvas = new Canvas(result);
627            rotateCanvas(canvas, outputX, outputY, rotation);
628            canvas.drawBitmap(mBitmap,
629                    rect, dest, new Paint(Paint.FILTER_BITMAP_FLAG));
630            return result;
631        }
632    }
633
634    private static void rotateCanvas(
635            Canvas canvas, int width, int height, int rotation) {
636        canvas.translate(width / 2, height / 2);
637        canvas.rotate(rotation);
638        if (((rotation / 90) & 0x01) == 0) {
639            canvas.translate(-width / 2, -height / 2);
640        } else {
641            canvas.translate(-height / 2, -width / 2);
642        }
643    }
644
645    private static void rotateRectangle(
646            Rect rect, int width, int height, int rotation) {
647        if (rotation == 0 || rotation == 360) return;
648
649        int w = rect.width();
650        int h = rect.height();
651        switch (rotation) {
652            case 90: {
653                rect.top = rect.left;
654                rect.left = height - rect.bottom;
655                rect.right = rect.left + h;
656                rect.bottom = rect.top + w;
657                return;
658            }
659            case 180: {
660                rect.left = width - rect.right;
661                rect.top = height - rect.bottom;
662                rect.right = rect.left + w;
663                rect.bottom = rect.top + h;
664                return;
665            }
666            case 270: {
667                rect.left = rect.top;
668                rect.top = width - rect.right;
669                rect.right = rect.left + h;
670                rect.bottom = rect.top + w;
671                return;
672            }
673            default: throw new AssertionError();
674        }
675    }
676
677    private void drawInTiles(Canvas canvas,
678            BitmapRegionDecoder decoder, Rect rect, Rect dest, int sample) {
679        int tileSize = TILE_SIZE * sample;
680        Rect tileRect = new Rect();
681        BitmapFactory.Options options = new BitmapFactory.Options();
682        options.inPreferredConfig = Config.ARGB_8888;
683        options.inSampleSize = sample;
684        canvas.translate(dest.left, dest.top);
685        canvas.scale((float) sample * dest.width() / rect.width(),
686                (float) sample * dest.height() / rect.height());
687        Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
688        for (int tx = rect.left, x = 0;
689                tx < rect.right; tx += tileSize, x += TILE_SIZE) {
690            for (int ty = rect.top, y = 0;
691                    ty < rect.bottom; ty += tileSize, y += TILE_SIZE) {
692                tileRect.set(tx, ty, tx + tileSize, ty + tileSize);
693                if (tileRect.intersect(rect)) {
694                    Bitmap bitmap;
695
696                    // To prevent concurrent access in GLThread
697                    synchronized (decoder) {
698                        bitmap = decoder.decodeRegion(tileRect, options);
699                    }
700                    canvas.drawBitmap(bitmap, x, y, paint);
701                    bitmap.recycle();
702                }
703            }
704        }
705    }
706
707    private void onBitmapRegionDecoderAvailable(
708            BitmapRegionDecoder regionDecoder) {
709
710        if (regionDecoder == null) {
711            Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
712            finish();
713            return;
714        }
715        mRegionDecoder = regionDecoder;
716        mUseRegionDecoder = true;
717        mState = STATE_LOADED;
718
719        BitmapFactory.Options options = new BitmapFactory.Options();
720        int width = regionDecoder.getWidth();
721        int height = regionDecoder.getHeight();
722        options.inSampleSize = BitmapUtils.computeSampleSize(width, height,
723                BitmapUtils.UNCONSTRAINED, BACKUP_PIXEL_COUNT);
724        mBitmap = regionDecoder.decodeRegion(
725                new Rect(0, 0, width, height), options);
726        mCropView.setDataModel(new TileImageViewAdapter(
727                mBitmap, regionDecoder), mMediaItem.getFullImageRotation());
728        if (mDoFaceDetection) {
729            mCropView.detectFaces(mBitmap);
730        } else {
731            mCropView.initializeHighlightRectangle();
732        }
733    }
734
735    private void onBitmapAvailable(Bitmap bitmap) {
736        if (bitmap == null) {
737            Toast.makeText(this, "fail to load image", Toast.LENGTH_SHORT).show();
738            finish();
739            return;
740        }
741        mUseRegionDecoder = false;
742        mState = STATE_LOADED;
743
744        mBitmap = bitmap;
745        BitmapFactory.Options options = new BitmapFactory.Options();
746        mCropView.setDataModel(new BitmapTileProvider(bitmap, 512),
747                mMediaItem.getRotation());
748        if (mDoFaceDetection) {
749            mCropView.detectFaces(bitmap);
750        } else {
751            mCropView.initializeHighlightRectangle();
752        }
753    }
754
755    private void setCropParameters() {
756        Bundle extras = getIntent().getExtras();
757        if (extras == null)
758            return;
759        int aspectX = extras.getInt(KEY_ASPECT_X, 0);
760        int aspectY = extras.getInt(KEY_ASPECT_Y, 0);
761        if (aspectX != 0 && aspectY != 0) {
762            mCropView.setAspectRatio((float) aspectX / aspectY);
763        }
764
765        float spotlightX = extras.getFloat(KEY_SPOTLIGHT_X, 0);
766        float spotlightY = extras.getFloat(KEY_SPOTLIGHT_Y, 0);
767        if (spotlightX != 0 && spotlightY != 0) {
768            mCropView.setSpotlightRatio(spotlightX, spotlightY);
769        }
770    }
771
772    private void initializeData() {
773        Bundle extras = getIntent().getExtras();
774
775        if (extras != null) {
776            if (extras.containsKey(KEY_NO_FACE_DETECTION)) {
777                mDoFaceDetection = !extras.getBoolean(KEY_NO_FACE_DETECTION);
778            }
779
780            mBitmapInIntent = extras.getParcelable(KEY_DATA);
781
782            if (mBitmapInIntent != null) {
783                mBitmapTileProvider =
784                        new BitmapTileProvider(mBitmapInIntent, MAX_BACKUP_IMAGE_SIZE);
785                mCropView.setDataModel(mBitmapTileProvider, 0);
786                if (mDoFaceDetection) {
787                    mCropView.detectFaces(mBitmapInIntent);
788                } else {
789                    mCropView.initializeHighlightRectangle();
790                }
791                mState = STATE_LOADED;
792                return;
793            }
794        }
795
796        mProgressDialog = ProgressDialog.show(
797                this, null, getString(R.string.loading_image), true, false);
798
799        mMediaItem = getMediaItemFromIntentData();
800        if (mMediaItem == null) return;
801
802        boolean supportedByBitmapRegionDecoder =
803            (mMediaItem.getSupportedOperations() & MediaItem.SUPPORT_FULL_IMAGE) != 0;
804        if (supportedByBitmapRegionDecoder) {
805            mLoadTask = getThreadPool().submit(new LoadDataTask(mMediaItem),
806                    new FutureListener<BitmapRegionDecoder>() {
807                public void onFutureDone(Future<BitmapRegionDecoder> future) {
808                    mLoadTask = null;
809                    BitmapRegionDecoder decoder = future.get();
810                    if (future.isCancelled()) {
811                        if (decoder != null) decoder.recycle();
812                        return;
813                    }
814                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
815                            MSG_LARGE_BITMAP, decoder));
816                }
817            });
818        } else {
819            mLoadBitmapTask = getThreadPool().submit(new LoadBitmapDataTask(mMediaItem),
820                    new FutureListener<Bitmap>() {
821                public void onFutureDone(Future<Bitmap> future) {
822                    mLoadBitmapTask = null;
823                    Bitmap bitmap = future.get();
824                    if (future.isCancelled()) {
825                        if (bitmap != null) bitmap.recycle();
826                        return;
827                    }
828                    mMainHandler.sendMessage(mMainHandler.obtainMessage(
829                            MSG_BITMAP, bitmap));
830                }
831            });
832        }
833    }
834
835    @Override
836    protected void onResume() {
837        super.onResume();
838        if (mState == STATE_INIT) initializeData();
839        if (mState == STATE_SAVING) onSaveClicked();
840
841        // TODO: consider to do it in GLView system
842        GLRoot root = getGLRoot();
843        root.lockRenderThread();
844        try {
845            mCropView.resume();
846        } finally {
847            root.unlockRenderThread();
848        }
849    }
850
851    @Override
852    protected void onPause() {
853        super.onPause();
854
855        Future<BitmapRegionDecoder> loadTask = mLoadTask;
856        if (loadTask != null && !loadTask.isDone()) {
857            // load in progress, try to cancel it
858            loadTask.cancel();
859            loadTask.waitDone();
860            mProgressDialog.dismiss();
861        }
862
863        Future<Bitmap> loadBitmapTask = mLoadBitmapTask;
864        if (loadBitmapTask != null && !loadBitmapTask.isDone()) {
865            // load in progress, try to cancel it
866            loadBitmapTask.cancel();
867            loadBitmapTask.waitDone();
868            mProgressDialog.dismiss();
869        }
870
871        Future<Intent> saveTask = mSaveTask;
872        if (saveTask != null && !saveTask.isDone()) {
873            // save in progress, try to cancel it
874            saveTask.cancel();
875            saveTask.waitDone();
876            mProgressDialog.dismiss();
877        }
878        GLRoot root = getGLRoot();
879        root.lockRenderThread();
880        try {
881            mCropView.pause();
882        } finally {
883            root.unlockRenderThread();
884        }
885    }
886
887    private MediaItem getMediaItemFromIntentData() {
888        Uri uri = getIntent().getData();
889        DataManager manager = getDataManager();
890        if (uri == null) {
891            Log.w(TAG, "no data given");
892            return null;
893        }
894        Path path = manager.findPathByUri(uri);
895        if (path == null) {
896            Log.w(TAG, "cannot get path for: " + uri);
897            return null;
898        }
899        return (MediaItem) manager.getMediaObject(path);
900    }
901
902    private class LoadDataTask implements Job<BitmapRegionDecoder> {
903        MediaItem mItem;
904
905        public LoadDataTask(MediaItem item) {
906            mItem = item;
907        }
908
909        public BitmapRegionDecoder run(JobContext jc) {
910            return mItem == null ? null : mItem.requestLargeImage().run(jc);
911        }
912    }
913
914    private class LoadBitmapDataTask implements Job<Bitmap> {
915        MediaItem mItem;
916
917        public LoadBitmapDataTask(MediaItem item) {
918            mItem = item;
919        }
920        public Bitmap run(JobContext jc) {
921            return mItem == null
922                    ? null
923                    : mItem.requestImage(MediaItem.TYPE_THUMBNAIL).run(jc);
924        }
925    }
926
927    private static final String[] EXIF_TAGS = {
928            ExifInterface.TAG_DATETIME,
929            ExifInterface.TAG_MAKE,
930            ExifInterface.TAG_MODEL,
931            ExifInterface.TAG_FLASH,
932            ExifInterface.TAG_GPS_LATITUDE,
933            ExifInterface.TAG_GPS_LONGITUDE,
934            ExifInterface.TAG_GPS_LATITUDE_REF,
935            ExifInterface.TAG_GPS_LONGITUDE_REF,
936            ExifInterface.TAG_GPS_ALTITUDE,
937            ExifInterface.TAG_GPS_ALTITUDE_REF,
938            ExifInterface.TAG_GPS_TIMESTAMP,
939            ExifInterface.TAG_GPS_DATESTAMP,
940            ExifInterface.TAG_WHITE_BALANCE,
941            ExifInterface.TAG_FOCAL_LENGTH,
942            ExifInterface.TAG_GPS_PROCESSING_METHOD};
943
944    private static void copyExif(MediaItem item, String destination, int newWidth, int newHeight) {
945        try {
946            ExifInterface newExif = new ExifInterface(destination);
947            PicasaSource.extractExifValues(item, newExif);
948            newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
949            newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
950            newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
951            newExif.saveAttributes();
952        } catch (Throwable t) {
953            Log.w(TAG, "cannot copy exif: " + item, t);
954        }
955    }
956
957    private static void copyExif(String source, String destination, int newWidth, int newHeight) {
958        try {
959            ExifInterface oldExif = new ExifInterface(source);
960            ExifInterface newExif = new ExifInterface(destination);
961
962            newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(newWidth));
963            newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(newHeight));
964            newExif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(0));
965
966            for (String tag : EXIF_TAGS) {
967                String value = oldExif.getAttribute(tag);
968                if (value != null) {
969                    newExif.setAttribute(tag, value);
970                }
971            }
972
973            // Handle some special values here
974            String value = oldExif.getAttribute(ExifInterface.TAG_APERTURE);
975            if (value != null) {
976                try {
977                    float aperture = Float.parseFloat(value);
978                    newExif.setAttribute(ExifInterface.TAG_APERTURE,
979                            String.valueOf((int) (aperture * 10 + 0.5f)) + "/10");
980                } catch (NumberFormatException e) {
981                    Log.w(TAG, "cannot parse aperture: " + value);
982                }
983            }
984
985            // TODO: The code is broken, need to fix the JHEAD lib
986            /*
987            value = oldExif.getAttribute(ExifInterface.TAG_EXPOSURE_TIME);
988            if (value != null) {
989                try {
990                    double exposure = Double.parseDouble(value);
991                    testToRational("test exposure", exposure);
992                    newExif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, value);
993                } catch (NumberFormatException e) {
994                    Log.w(TAG, "cannot parse exposure time: " + value);
995                }
996            }
997
998            value = oldExif.getAttribute(ExifInterface.TAG_ISO);
999            if (value != null) {
1000                try {
1001                    int iso = Integer.parseInt(value);
1002                    newExif.setAttribute(ExifInterface.TAG_ISO, String.valueOf(iso) + "/1");
1003                } catch (NumberFormatException e) {
1004                    Log.w(TAG, "cannot parse exposure time: " + value);
1005                }
1006            }*/
1007            newExif.saveAttributes();
1008        } catch (Throwable t) {
1009            Log.w(TAG, "cannot copy exif: " + source, t);
1010        }
1011    }
1012}
1013