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