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