Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial import

  • Loading branch information...
commit ccf3eb59dae74e67bff3436db984c8948fc19954 1 parent ab076a5
@lvillani authored
Showing with 3,511 additions and 0 deletions.
  1. +11 −0 .gitignore
  2. +1 −0  .pc/.quilt_patches
  3. +1 −0  .pc/.quilt_series
  4. +1 −0  .pc/.version
  5. +1 −0  AUTHORS
  6. +8 −0 AndroidManifest.xml
  7. +202 −0 COPYING
  8. +14 −0 Makefile
  9. +12 −0 default.properties
  10. +29 −0 patches/01-cropimage-layout-fix.patch
  11. +84 −0 patches/02-cropimage-minimize-dependencies.patch
  12. +2 −0  patches/series
  13. BIN  res/drawable-hdpi/camera_crop_height.png
  14. BIN  res/drawable-hdpi/camera_crop_width.png
  15. BIN  res/drawable-hdpi/indicator_autocrop.png
  16. BIN  res/drawable-mdpi/camera_crop_height.png
  17. BIN  res/drawable-mdpi/camera_crop_width.png
  18. BIN  res/drawable-mdpi/indicator_autocrop.png
  19. +57 −0 res/layout/cropimage.xml
  20. +409 −0 res/values/strings.xml
  21. +195 −0 src/com/android/camera/BitmapManager.java
  22. +794 −0 src/com/android/camera/CropImage.java
  23. +412 −0 src/com/android/camera/HighlightView.java
  24. +407 −0 src/com/android/camera/ImageViewTouchBase.java
  25. +90 −0 src/com/android/camera/MonitoredActivity.java
  26. +26 −0 src/com/android/camera/NoSearchActivity.java
  27. +96 −0 src/com/android/camera/RotateBitmap.java
  28. +438 −0 src/com/android/camera/Util.java
  29. +80 −0 src/com/android/camera/gallery/IImage.java
  30. +96 −0 src/com/android/camera/gallery/IImageList.java
  31. +27 −0 tools/import-from-gallery.sh
  32. +18 −0 tools/import.list
View
11 .gitignore
@@ -0,0 +1,11 @@
+.DS_Store
+.classpath
+.project
+Thumbs.db
+desktop.ini
+
+.pc/
+.settings/
+bin/
+gen/
+target/
View
1  .pc/.quilt_patches
@@ -0,0 +1 @@
+patches
View
1  .pc/.quilt_series
@@ -0,0 +1 @@
+series
View
1  .pc/.version
@@ -0,0 +1 @@
+2
View
1  AUTHORS
@@ -0,0 +1 @@
+Lorenzo Villani <lorenzo@villani.me>
View
8 AndroidManifest.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:versionCode="1"
+ android:versionName="1.0"
+ package="com.android.gallery">
+ <uses-sdk android:minSdkVersion="7" />
+</manifest>
View
202 COPYING
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
View
14 Makefile
@@ -0,0 +1,14 @@
+QUILT = quilt
+
+all: apply
+
+
+apply:
+ $(QUILT) push -a
+
+
+unapply:
+ $(QUILT) pop -a
+
+
+.PHONY: all apply unapply
View
12 default.properties
@@ -0,0 +1,12 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system use,
+# "build.properties", and override values to adapt the script to your
+# project structure.
+
+# Project target.
+target=android-7
+android.library=true
View
29 patches/01-cropimage-layout-fix.patch
@@ -0,0 +1,29 @@
+Index: android-cropimage/res/layout/cropimage.xml
+===================================================================
+--- android-cropimage.orig/res/layout/cropimage.xml 2011-07-20 21:38:05.724325996 +0200
++++ android-cropimage/res/layout/cropimage.xml 2011-07-20 21:38:22.082758767 +0200
+@@ -15,18 +15,18 @@
+ -->
+
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+- android:layout_width="match_parent"
+- android:layout_height="match_parent">
++ android:layout_width="fill_parent"
++ android:layout_height="fill_parent">
+
+ <RelativeLayout
+- android:layout_width="match_parent"
+- android:layout_height="match_parent"
++ android:layout_width="fill_parent"
++ android:layout_height="fill_parent"
+ android:orientation="horizontal">
+
+ <view class="com.android.camera.CropImageView" android:id="@+id/image"
+ android:background="#55000000"
+- android:layout_width="match_parent"
+- android:layout_height="match_parent"
++ android:layout_width="fill_parent"
++ android:layout_height="fill_parent"
+ android:layout_x="0dip"
+ android:layout_y="0dip"
+ />
View
84 patches/02-cropimage-minimize-dependencies.patch
@@ -0,0 +1,84 @@
+Index: android-cropimage/src/com/android/camera/CropImage.java
+===================================================================
+--- android-cropimage.orig/src/com/android/camera/CropImage.java 2011-07-20 21:47:57.882158360 +0200
++++ android-cropimage/src/com/android/camera/CropImage.java 2011-07-20 21:52:12.882168456 +0200
+@@ -97,8 +97,6 @@
+
+ mImageView = (CropImageView) findViewById(R.id.image);
+
+- MenuHelper.showStorageToast(this);
+-
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+
+@@ -131,20 +129,6 @@
+ }
+
+ if (mBitmap == null) {
+- Uri target = intent.getData();
+- mAllImages = ImageManager.makeImageList(mContentResolver, target,
+- ImageManager.SORT_ASCENDING);
+- mImage = mAllImages.getImageForUri(target);
+- if (mImage != null) {
+- // Don't read in really large bitmaps. Use the (big) thumbnail
+- // instead.
+- // TODO when saving the resulting bitmap use the
+- // decode/crop/encode api so we don't lose any resolution.
+- mBitmap = mImage.thumbBitmap(IImage.ROTATE_AS_NEEDED);
+- }
+- }
+-
+- if (mBitmap == null) {
+ finish();
+ return;
+ }
+@@ -344,49 +328,6 @@
+ Log.e(TAG, "Failed to set wallpaper.", e);
+ setResult(RESULT_CANCELED);
+ }
+- } else {
+- Bundle extras = new Bundle();
+- extras.putString("rect", mCrop.getCropRect().toString());
+-
+- File oldPath = new File(mImage.getDataPath());
+- File directory = new File(oldPath.getParent());
+-
+- int x = 0;
+- String fileName = oldPath.getName();
+- fileName = fileName.substring(0, fileName.lastIndexOf("."));
+-
+- // Try file-1.jpg, file-2.jpg, ... until we find a filename which
+- // does not exist yet.
+- while (true) {
+- x += 1;
+- String candidate = directory.toString()
+- + "/" + fileName + "-" + x + ".jpg";
+- boolean exists = (new File(candidate)).exists();
+- if (!exists) {
+- break;
+- }
+- }
+-
+- try {
+- int[] degree = new int[1];
+- Uri newUri = ImageManager.addImage(
+- mContentResolver,
+- mImage.getTitle(),
+- mImage.getDateTaken(),
+- null, // TODO this null is going to cause us to lose
+- // the location (gps).
+- directory.toString(), fileName + "-" + x + ".jpg",
+- croppedImage, null,
+- degree);
+-
+- setResult(RESULT_OK, new Intent()
+- .setAction(newUri.toString())
+- .putExtras(extras));
+- } catch (Exception ex) {
+- // basically ignore this or put up
+- // some ui saying we failed
+- Log.e(TAG, "store image fail, continue anyway", ex);
+- }
+ }
+
+ final Bitmap b = croppedImage;
View
2  patches/series
@@ -0,0 +1,2 @@
+01-cropimage-layout-fix.patch
+02-cropimage-minimize-dependencies.patch
View
BIN  res/drawable-hdpi/camera_crop_height.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  res/drawable-hdpi/camera_crop_width.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  res/drawable-hdpi/indicator_autocrop.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  res/drawable-mdpi/camera_crop_height.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  res/drawable-mdpi/camera_crop_width.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  res/drawable-mdpi/indicator_autocrop.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
57 res/layout/cropimage.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <view class="com.android.camera.CropImageView" android:id="@+id/image"
+ android:background="#55000000"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_x="0dip"
+ android:layout_y="0dip"
+ />
+ <RelativeLayout android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true">
+ <Button
+ android:id="@+id/save"
+ android:layout_width="100dip"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:text="@string/crop_save_text"
+ />
+ <Button
+ android:id="@+id/discard"
+ android:layout_width="100dip"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:text="@string/crop_discard_text"
+ />
+ </RelativeLayout>
+ </RelativeLayout>
+
+</FrameLayout>
View
409 res/values/strings.xml
@@ -0,0 +1,409 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- General strings -->
+
+ <!-- label for the icon meaning 'show me all the images' -->
+ <string name="all_images">All pictures</string>
+
+ <!-- label for the icon meaning 'show me all the videos' -->
+ <string name="all_videos">All videos</string>
+
+ <!-- label for the icon meaning 'show me all the images that were taken with the camera' -->
+ <string name="camera_label">Camera</string>
+
+ <!-- label for the 'pictures application shown in the top level 'all applications' -->
+ <string name="gallery_picker_label">Gallery</string>
+
+ <!-- label for the gallery application (displayed when the user needs to
+ choose an application to pick a picture or a video) -->
+ <string name="gallery_label">Gallery</string>
+
+ <!-- label for the folder that contains Camera pictures in the gallery -->
+ <string name="gallery_camera_bucket_name">Camera pictures</string>
+
+ <!-- label for the folder that contains Camera videos in the gallery -->
+ <string name="gallery_camera_videos_bucket_name">Camera videos</string>
+
+ <!-- label for the folder that contains Camera videos in the gallery -->
+ <string name="gallery_camera_media_bucket_name">Camera media</string>
+
+ <!-- menu pick: crop the currently selected image -->
+ <string name="crop_label">Crop picture</string>
+
+ <!-- menu pick: view the currently selected image -->
+ <string name="view_label">View picture</string>
+
+ <!-- menu pick: go to the preferences screen for the camera or image gallery -->
+ <string name="preferences_label">Camera settings</string>
+
+ <!-- alert to the user to wait for some operation to complete -->
+ <string name="wait">Please wait\u2026</string>
+
+ <!-- alert to the user to that shared storage must be available before using the camera [CHAR LIMIT=NONE] -->
+ <string name="no_storage" product="nosdcard">Please mount the shared storage before using the camera.</string>
+ <!-- alert to the user to that an SD card must be installed before using the camera -->
+ <string name="no_storage" product="default">Please insert an SD card before using the camera.</string>
+
+ <!-- alert to the user to that the shared storage is too full to complete the operation [CHAR LIMIT=NONE] -->
+ <string name="not_enough_space" product="nosdcard">Your shared storage is full.</string>
+ <!-- alert to the user to that the SD card is too full to complete the operation -->
+ <string name="not_enough_space" product="default">Your SD card is full.</string>
+
+ <!-- alert to the user to that the shared storage is being disk-checked [CHAR LIMIT=NONE] -->
+ <string name="preparing_sd" product="nosdcard">Preparing shared storage\u2026</string>
+ <!-- alert to the user to that the SD card is being disk-checked -->
+ <string name="preparing_sd" product="default">Preparing SD card\u2026</string>
+
+ <!-- Toast/alert after saving wallpaper -->
+ <string name="wallpaper">Setting wallpaper, please wait\u2026</string>
+
+ <!-- Settings stuff -->
+
+ <!-- Toast/alert that the image is being saved to the SD card -->
+ <string name="savingImage">Saving picture\u2026</string>
+
+ <!-- Toast/alert that the face detection is being run -->
+ <string name="runningFaceDetection">Please wait\u2026</string>
+
+
+ <!-- Menu items: -->
+ <!-- menu pick to view the currently selected image -->
+ <string name="view">View</string>
+
+ <!-- menu pick to view the details of the currently selected image -->
+ <string name="details">Details</string>
+
+ <!-- menu pick to show the location of the currently selected image on maps-->
+ <string name="show_on_map">Show on Maps</string>
+
+ <!-- menu pick to rotate the currently selected image (brings up submenu) -->
+ <string name="rotate">Rotate</string>
+
+ <!-- menu pick to rotate the currently selected image to the left -->
+ <string name="rotate_left">Rotate left</string>
+
+ <!-- menu pick to rotate the currently selected image to the right -->
+ <string name="rotate_right">Rotate right</string>
+
+ <!-- menu pick to start a slide show -->
+ <string name="slide_show">Slideshow</string>
+
+ <!-- menu pick to enter multiselect mode -->
+ <string name="multiselect">Multiselect</string>
+
+ <!-- menu pick to go to camera mode to capture a picture -->
+ <string name="capture_picture">Capture picture</string>
+ <!-- menu pick to go to video mode to capture a video -->
+ <string name="capture_video">Capture video</string>
+
+ <!-- button indicating that the cropped image should be saved -->
+ <string name="crop_save_text">Save</string>
+ <!-- button indicating that the cropped image should be reverted back to the original -->
+ <string name="crop_discard_text">Discard</string>
+
+ <!-- Confirmation dialog title after deleting a picture -->
+ <string name="confirm_delete_title">Delete</string>
+ <!-- Confirmation dialog message after deleting a picture -->
+ <string name="confirm_delete_message">The picture will be deleted.</string>
+ <!-- Confirmation dialog message after deleting a video -->
+ <string name="confirm_delete_video_message">The video will be deleted.</string>
+
+ <!-- Confirmation dialog message after deleting a multiple media files -->
+ <string name="confirm_delete_multiple_message">These media files will be deleted.</string>
+
+ <!-- button indicating that the picture just taken should be deleted -->
+ <string name="camera_toss">Delete</string>
+
+ <!-- button indicating that the picture just taken should be shared by email, mms, etc -->
+ <string name="camera_share">Share</string>
+
+ <!-- button indicating that the picture just taken should be set as a contact photo, wallpaper, etc -->
+ <string name="camera_set">Set as</string>
+
+ <!-- button indicating that the video just taken should be played -->
+ <string name="camera_play">Play</string>
+
+ <!-- button indicating that the video just taken should be accepted as an attachment -->
+ <string name="camera_attach">Attach</string>
+
+ <!-- button indicating that the video recording session should be canceled -->
+ <string name="camera_cancel">Cancel</string>
+
+ <!-- button indicating that the picture just taken should be cropped -->
+ <string name="camera_crop">Crop</string>
+
+ <!-- Toast after trying to share a picture indicating that there are no applications which are capable of so doing. -->
+ <string name="no_way_to_share_image">No application available to share the picture.</string>
+
+ <!-- Toast after trying to share a video indicating that there are no applications which are capable of so doing. -->
+ <string name="no_way_to_share_video">No application available to share the video.</string>
+
+ <!-- Toast after trying to share multiple media files indicating that there are no applications which are capable of so doing. -->
+ <string name="no_way_to_share">No application available to share the media file(s).</string>
+
+ <!-- Menu item for playing the video. -->
+ <string name="video_play">Play</string>
+
+ <!-- String indicating an action of picking a picture to use as wallpaper (e.g. set wallpaper from "Pictures") -->
+ <string name="camera_pick_wallpaper">Pictures</string>
+ <string name="camera_setas_wallpaper">Wallpaper</string>
+
+ <!-- Settings screen, section heading -->
+ <string name="pref_gallery_category">General settings</string>
+
+ <!-- Settings screen, section heading -->
+ <string name="pref_slideshow_category">Slideshow settings</string>
+
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_size_title">Display size</string>
+
+ <!-- Settings screen, title for preference for image size to be used in the im -->
+ <string name="pref_gallery_size_summary">Select the display size of pictures and videos</string>
+ <!-- Title of dialog that appears after selecting Picture size setting option -->
+ <string name="pref_gallery_size_dialogtitle">Picture size</string>
+ <!-- Options in dialog that appears after selecting Picture size setting option -->
+ <string-array name="pref_gallery_size_choices">
+ <!-- size choice of "large" -->
+ <item>Large</item>
+ <!-- size choice of "small" -->
+ <item>Small</item>
+ </string-array>
+ <!-- Option values in dialog that appears after selecting Picture size setting option -->
+ <string-array name="pref_gallery_size_values" translatable="false">
+ <item>1</item>
+ <item>0</item>
+ </string-array>
+ <!-- Default option value in dialog that appears after selecting Picture size setting option -->
+ <string name="default_value_pref_gallery_size" translatable="false">1</string>
+ <!-- Settings screen, setting option name -->
+ <string name="pref_gallery_sort_title">Sort order</string>
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_sort_summary">Select the sort order of pictures and videos</string>
+ <!-- Title of dialog that appears after selecting Picture sort setting option -->
+ <string name="pref_gallery_sort_dialogtitle">Picture sort</string>
+ <!-- Options in dialog that appears after selecting Picture sort setting option -->
+ <string-array name="pref_gallery_sort_choices">
+ <!-- Preference choice to show "newest first" -->
+ <item>Newest first</item>
+ <!-- Preference choice to show "newest last" -->
+ <item>Newest last</item>
+ </string-array>
+ <!-- Option values in dialog that appears after selecting Picture sort setting option -->
+ <string-array name="pref_gallery_sort_values" translatable="false">
+ <item>descending</item>
+ <item>ascending</item>
+ </string-array>
+ <!-- Default option value in dialog that appears after selecting Picture sort setting option -->
+ <string name="default_value_pref_gallery_sort" translatable="false">descending</string>
+ <!-- Settings screen, setting option name -->
+ <string name="pref_gallery_slideshow_interval_title">Slideshow interval</string>
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_slideshow_interval_summary">Select how long each slide displays in the show</string>
+ <!-- Title of dialog that appears after selecting Slideshow interval setting option -->
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Slideshow interval</string>
+ <!-- Options in dialog that appears after selecting Slideshow interval setting option -->
+ <string-array name="pref_gallery_slideshow_interval_choices">
+ <!-- slide show interval "N seconds" where N is 2 -->
+ <item>2 seconds</item>
+ <!-- slide show interval "N seconds" where N is 3 -->
+ <item>3 seconds</item>
+ <!-- slide show interval "N seconds" where N is 4 -->
+ <item>4 seconds</item>
+ </string-array>
+ <!-- Option values in dialog that appears after selecting Slideshow interval setting option -->
+ <string-array name="pref_gallery_slideshow_interval_values" translatable="false">
+ <item>"2"</item>
+ <item>"3"</item>
+ <item>"4"</item>
+ </string-array>
+ <!-- Default option value in dialog that appears after selecting Slideshow interval setting option -->
+ <string name="default_value_pref_gallery_slideshow_interval" translatable="false">"2"</string>
+ <!-- Settings screen, setting option name -->
+ <string name="pref_gallery_slideshow_transition_title">Slideshow transition</string>
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_slideshow_transition_summary">Select the effect used when moving from one slide to the next</string>
+ <!-- Title of dialog that appears after selecting Slideshow transition setting option -->
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Slideshow transition</string>
+ <!-- Options in dialog that appears after selecting Slideshow transition
+ setting option -->
+ <string-array name="pref_gallery_slideshow_transition_choices">
+ <!-- Slide show transition to fade in and fade out -->
+ <item>Fade in &amp; out</item>
+ <!-- Slide show transition to slide in and out from the left and right -->
+ <item>Slide left - right</item>
+ <!-- Slide show transition to slide in and out from the top and bottom -->
+ <item>Slide up - down</item>
+ <!-- Slide show transition to be chosen randomly -->
+ <item>Random selection</item>
+ </string-array>
+ <!-- Option values in dialog that appears after selecting Slideshow transition setting option -->
+ <string-array name="pref_gallery_slideshow_transition_values" translatable="false">
+ <item>"0"</item>
+ <item>"1"</item>
+ <item>"2"</item>
+ <item>"-1"</item>
+ </string-array>
+ <!-- Default option value in dialog that appears after selecting Slideshow transition setting option -->
+ <string name="default_value_pref_gallery_slideshow_transition" translatable="false">"0"</string>
+
+ <!-- Settings screen, setting check box name -->
+ <string name="pref_gallery_slideshow_repeat_title">Repeat slideshow</string>
+
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_slideshow_repeat_summary">Play slideshow more than once</string>
+
+ <!-- Settings screen, setting check box name -->
+ <string name="pref_gallery_slideshow_shuffle_title">Shuffle slides</string>
+
+ <!-- Settings screen, setting summary text -->
+ <string name="pref_gallery_slideshow_shuffle_summary">Show pictures in random order</string>
+
+ <!-- Menu item to go to the settings screen -->
+ <string name="camerasettings">Settings</string>
+
+ <!-- Text message indicating that there are no pictures or videos in a particular
+ bucket on the SD card -->
+ <string name="image_gallery_NoImageView_text">No media found.</string>
+
+ <!-- Preference title for whether the user should be prompted form confirmation when deleting images -->
+ <string name="pref_gallery_confirm_delete_title">Confirm deletions</string>
+
+ <!-- Preference summary for whether the user should be prompted form confirmation when deleting images -->
+ <string name="pref_gallery_confirm_delete_summary">Show confirmation before deleting a picture or video</string>
+
+ <!-- Message to show when there's no lat/lng information in the image -->
+ <string name="no_location_image">No Location information contained in this image.</string>
+ <!-- Title of Details dialog -->
+ <string name="details_panel_title">Details</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_file_size">File size:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_resolution">Resolution:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_make">Manufacturer:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_model">Model:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_whitebalance">WhiteBalance:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_latitude">GPS Latitude:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_longitude">GPS Longitude:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_image_location">Location:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_duration">Duration:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_date_taken">Date taken:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_frame_rate">Frame rate:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_bit_rate">Bit rate:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_codec">Codec:</string>
+ <!-- Label in message of Details dialog -->
+ <string name="details_format">Format:</string>
+
+ <!-- Used to format image dimensions in Details dialog. e.g. 64 x 64 -->
+ <string name="details_dimension_x"><xliff:g id="width" example="640">%1$d</xliff:g> x <xliff:g id="height" example="480">%2$d</xliff:g></string>
+ <!-- Used to format short video duration in Details dialog. minutes:seconds e.g. 00:30 -->
+ <string name="details_ms"><xliff:g id="minutes">%1$02d</xliff:g>:<xliff:g id="seconds">%2$02d</xliff:g></string>
+ <!-- Used to format video duration in Details dialog. hours:minutes:seconds e.g. 0:21:30 -->
+ <string name="details_hms"><xliff:g id="hours">%1$d</xliff:g>:<xliff:g id="minutes">%2$02d</xliff:g>:<xliff:g id="seconds">%3$02d</xliff:g></string>
+ <!-- Unit of measure in the Details dialog frames per second. e.g. 20 fps -->
+ <string name="details_fps"><xliff:g id="frames" example="30">%1$d</xliff:g> fps</string>
+ <!-- Unit of measure in the Details dialog K bits per second. e.g. 192 Kbps -->
+ <string name="details_kbps"><xliff:g id="speed" example="192">%1$d</xliff:g> Kbps</string>
+ <!-- Unit of measure in the Details dialog M bits per second. e.g. 2.3 Mbps -->
+ <string name="details_mbps"><xliff:g id="speed" example="2.3">%1$g</xliff:g> Mbps</string>
+ <!-- Details dialog "OK" button. Dismisses dialog. -->
+ <string name="details_ok">OK</string>
+
+ <!-- Text of context menu when an image is selected -->
+ <string name="context_menu_header">Picture options</string>
+ <!-- Text of context menu when a video is selected -->
+ <string name="video_context_menu_header">Video options</string>
+ <!-- Hint that appears when cropping an image with more than one face -->
+ <string name="multiface_crop_help">Tap a face to begin.</string>
+
+ <!-- Activity title when in the image gallery to see pictures -->
+ <string name="photos_gallery_title">Gallery</string>
+
+ <!-- Activity title when in the image gallery to select a picture -->
+ <string name="pick_photos_gallery_title">Select picture</string>
+
+ <!-- Activity title when in the image gallery to see videos -->
+ <string name="videos_gallery_title">Gallery</string>
+
+ <!-- Activity title when in the image gallery to select a video -->
+ <string name="pick_videos_gallery_title">Select video</string>
+
+ <!-- Displayed in the title of the dialog for things to do with a picture that
+ is to be sent to another application: -->
+ <string name="sendImage">Share picture via</string>
+
+ <!-- Displayed in the title of the dialog for things to do with a picture that
+ is to be "set as" (e.g. set as contact photo or set as wallpaper) -->
+ <string name="setImage">Set picture as</string>
+
+ <!-- Displayed in the title of the dialog for things to do with a video that
+ is to be sent to another application. -->
+ <string name="sendVideo">Share video via</string>
+
+ <!-- Displayed in the title of the dialog for things to do with media files that
+ are to be sent to another application: -->
+ <string name="send_media_files">Share media files via</string>
+
+ <!-- Activity label. This might show up in the activity-picker -->
+ <string name="movieviewlabel">Movies</string>
+ <!-- shown in the video player view while the video is being loaded, before it starts playing -->
+ <string name="loading_video">Loading video\u2026</string>
+
+ <!-- Movie View Resume Playing dialog title -->
+ <string name="resume_playing_title">Resume video</string>
+
+ <!-- Movie View Start Playing dialog title -->
+ <string name="resume_playing_message">Resume playing from <xliff:g id="duration" example="5:30">%s</xliff:g> ?</string>
+
+ <!-- Movie View Start Playing button "Resume from bookmark" -->
+ <string name="resume_playing_resume">Resume playing</string>
+
+ <!-- Movie View Start Playing button "Beginning" -->
+ <string name="resume_playing_restart">Start over</string>
+
+ <!-- Title for picture frame gadget to show in list of all available gadgets -->
+ <string name="gadget_title">Picture frame</string>
+
+ <!-- Title for the file information dialog -->
+ <string name="file_info_title">File info:</string>
+
+ <!-- The video is execeed the size limit of a MMS, and suggest user to record a shorter length clip -->
+ <string name="video_exceed_mms_limit">The video you recorded is too large to send via MMS. Try recording a shorter length clip.</string>
+
+ <!-- The title shown on the button which share the selected images by gmail, mms, and etc. -->
+ <string name="multiselect_share">Share</string>
+
+ <!-- The title shown on the button which will delete all the selected images -->
+ <string name="multiselect_delete">Delete</string>
+
+ <!-- Title shown on the button which will close the multiselction mode -->
+ <string name="multiselect_cancel">Cancel</string>
+
+ <!-- The messsage shown on progress dialog when deleting images -->
+ <string name="delete_images_message">Deleting images, please wait\u2026</string>
+</resources>
View
195 src/com/android/camera/BitmapManager.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import android.content.ContentResolver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.util.WeakHashMap;
+
+/**
+ * This class provides several utilities to cancel bitmap decoding.
+ *
+ * The function decodeFileDescriptor() is used to decode a bitmap. During
+ * decoding if another thread wants to cancel it, it calls the function
+ * cancelThreadDecoding() specifying the Thread which is in decoding.
+ *
+ * cancelThreadDecoding() is sticky until allowThreadDecoding() is called.
+ */
+public class BitmapManager {
+ private static final String TAG = "BitmapManager";
+ private static enum State {CANCEL, ALLOW}
+ private static class ThreadStatus {
+ public State mState = State.ALLOW;
+ public BitmapFactory.Options mOptions;
+ public boolean mThumbRequesting;
+ @Override
+ public String toString() {
+ String s;
+ if (mState == State.CANCEL) {
+ s = "Cancel";
+ } else if (mState == State.ALLOW) {
+ s = "Allow";
+ } else {
+ s = "?";
+ }
+ s = "thread state = " + s + ", options = " + mOptions;
+ return s;
+ }
+ }
+
+ private final WeakHashMap<Thread, ThreadStatus> mThreadStatus =
+ new WeakHashMap<Thread, ThreadStatus>();
+
+ private static BitmapManager sManager = null;
+
+ private BitmapManager() {
+ }
+
+ /**
+ * Get thread status and create one if specified.
+ */
+ private synchronized ThreadStatus getOrCreateThreadStatus(Thread t) {
+ ThreadStatus status = mThreadStatus.get(t);
+ if (status == null) {
+ status = new ThreadStatus();
+ mThreadStatus.put(t, status);
+ }
+ return status;
+ }
+
+ /**
+ * The following three methods are used to keep track of
+ * BitmapFaction.Options used for decoding and cancelling.
+ */
+ private synchronized void setDecodingOptions(Thread t,
+ BitmapFactory.Options options) {
+ getOrCreateThreadStatus(t).mOptions = options;
+ }
+
+ synchronized void removeDecodingOptions(Thread t) {
+ ThreadStatus status = mThreadStatus.get(t);
+ status.mOptions = null;
+ }
+
+ /**
+ * The following three methods are used to keep track of which thread
+ * is being disabled for bitmap decoding.
+ */
+ public synchronized boolean canThreadDecoding(Thread t) {
+ ThreadStatus status = mThreadStatus.get(t);
+ if (status == null) {
+ // allow decoding by default
+ return true;
+ }
+
+ boolean result = (status.mState != State.CANCEL);
+ return result;
+ }
+
+ public synchronized void allowThreadDecoding(Thread t) {
+ getOrCreateThreadStatus(t).mState = State.ALLOW;
+ }
+
+ public synchronized void cancelThreadDecoding(Thread t, ContentResolver cr) {
+ ThreadStatus status = getOrCreateThreadStatus(t);
+ status.mState = State.CANCEL;
+ if (status.mOptions != null) {
+ status.mOptions.requestCancelDecode();
+ }
+
+ // Wake up threads in waiting list
+ notifyAll();
+
+ // Since our cancel request can arrive MediaProvider earlier than getThumbnail request,
+ // we use mThumbRequesting flag to make sure our request does cancel the request.
+ try {
+ synchronized (status) {
+ while (status.mThumbRequesting) {
+ Images.Thumbnails.cancelThumbnailRequest(cr, -1, t.getId());
+ Video.Thumbnails.cancelThumbnailRequest(cr, -1, t.getId());
+ status.wait(200);
+ }
+ }
+ } catch (InterruptedException ex) {
+ // ignore it.
+ }
+ }
+
+ public Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
+ BitmapFactory.Options options, boolean isVideo) {
+ Thread t = Thread.currentThread();
+ ThreadStatus status = getOrCreateThreadStatus(t);
+
+ if (!canThreadDecoding(t)) {
+ Log.d(TAG, "Thread " + t + " is not allowed to decode.");
+ return null;
+ }
+
+ try {
+ synchronized (status) {
+ status.mThumbRequesting = true;
+ }
+ if (isVideo) {
+ return Video.Thumbnails.getThumbnail(cr, origId, t.getId(),
+ kind, null);
+ } else {
+ return Images.Thumbnails.getThumbnail(cr, origId, t.getId(),
+ kind, null);
+ }
+ } finally {
+ synchronized (status) {
+ status.mThumbRequesting = false;
+ status.notifyAll();
+ }
+ }
+ }
+
+ public static synchronized BitmapManager instance() {
+ if (sManager == null) {
+ sManager = new BitmapManager();
+ }
+ return sManager;
+ }
+
+ /**
+ * The real place to delegate bitmap decoding to BitmapFactory.
+ */
+ public Bitmap decodeFileDescriptor(FileDescriptor fd,
+ BitmapFactory.Options options) {
+ if (options.mCancel) {
+ return null;
+ }
+
+ Thread thread = Thread.currentThread();
+ if (!canThreadDecoding(thread)) {
+ Log.d(TAG, "Thread " + thread + " is not allowed to decode.");
+ return null;
+ }
+
+ setDecodingOptions(thread, options);
+ Bitmap b = BitmapFactory.decodeFileDescriptor(fd, null, options);
+
+ removeDecodingOptions(thread);
+ return b;
+ }
+}
View
794 src/com/android/camera/CropImage.java
@@ -0,0 +1,794 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import com.android.gallery.R;
+
+import com.android.camera.gallery.IImage;
+import com.android.camera.gallery.IImageList;
+
+import android.app.WallpaperManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.media.FaceDetector;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.MediaStore;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * The activity can crop specific region of interest from an image.
+ */
+public class CropImage extends MonitoredActivity {
+ private static final String TAG = "CropImage";
+
+ // These are various options can be specified in the intent.
+ private Bitmap.CompressFormat mOutputFormat =
+ Bitmap.CompressFormat.JPEG; // only used with mSaveUri
+ private Uri mSaveUri = null;
+ private boolean mSetWallpaper = false;
+ private int mAspectX, mAspectY;
+ private boolean mDoFaceDetection = true;
+ private boolean mCircleCrop = false;
+ private final Handler mHandler = new Handler();
+
+ // These options specifiy the output image size and whether we should
+ // scale the output to fit it (or just crop it).
+ private int mOutputX, mOutputY;
+ private boolean mScale;
+ private boolean mScaleUp = true;
+
+ boolean mWaitingToPick; // Whether we are wait the user to pick a face.
+ boolean mSaving; // Whether the "save" button is already clicked.
+
+ private CropImageView mImageView;
+ private ContentResolver mContentResolver;
+
+ private Bitmap mBitmap;
+ HighlightView mCrop;
+
+ private IImageList mAllImages;
+ private IImage mImage;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ mContentResolver = getContentResolver();
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.cropimage);
+
+ mImageView = (CropImageView) findViewById(R.id.image);
+
+ MenuHelper.showStorageToast(this);
+
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+
+ if (extras != null) {
+ if (extras.getString("circleCrop") != null) {
+ mCircleCrop = true;
+ mAspectX = 1;
+ mAspectY = 1;
+ }
+ mSaveUri = (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT);
+ if (mSaveUri != null) {
+ String outputFormatString = extras.getString("outputFormat");
+ if (outputFormatString != null) {
+ mOutputFormat = Bitmap.CompressFormat.valueOf(
+ outputFormatString);
+ }
+ } else {
+ mSetWallpaper = extras.getBoolean("setWallpaper");
+ }
+ mBitmap = (Bitmap) extras.getParcelable("data");
+ mAspectX = extras.getInt("aspectX");
+ mAspectY = extras.getInt("aspectY");
+ mOutputX = extras.getInt("outputX");
+ mOutputY = extras.getInt("outputY");
+ mScale = extras.getBoolean("scale", true);
+ mScaleUp = extras.getBoolean("scaleUpIfNeeded", true);
+ mDoFaceDetection = extras.containsKey("noFaceDetection")
+ ? !extras.getBoolean("noFaceDetection")
+ : true;
+ }
+
+ if (mBitmap == null) {
+ Uri target = intent.getData();
+ mAllImages = ImageManager.makeImageList(mContentResolver, target,
+ ImageManager.SORT_ASCENDING);
+ mImage = mAllImages.getImageForUri(target);
+ if (mImage != null) {
+ // Don't read in really large bitmaps. Use the (big) thumbnail
+ // instead.
+ // TODO when saving the resulting bitmap use the
+ // decode/crop/encode api so we don't lose any resolution.
+ mBitmap = mImage.thumbBitmap(IImage.ROTATE_AS_NEEDED);
+ }
+ }
+
+ if (mBitmap == null) {
+ finish();
+ return;
+ }
+
+ // Make UI fullscreen.
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+
+ findViewById(R.id.discard).setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+
+ findViewById(R.id.save).setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ onSaveClicked();
+ }
+ });
+
+ startFaceDetection();
+ }
+
+ private void startFaceDetection() {
+ if (isFinishing()) {
+ return;
+ }
+
+ mImageView.setImageBitmapResetBase(mBitmap, true);
+
+ Util.startBackgroundJob(this, null,
+ getResources().getString(R.string.runningFaceDetection),
+ new Runnable() {
+ public void run() {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Bitmap b = (mImage != null)
+ ? mImage.fullSizeBitmap(IImage.UNCONSTRAINED,
+ 1024 * 1024)
+ : mBitmap;
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (b != mBitmap && b != null) {
+ mImageView.setImageBitmapResetBase(b, true);
+ mBitmap.recycle();
+ mBitmap = b;
+ }
+ if (mImageView.getScale() == 1F) {
+ mImageView.center(true, true);
+ }
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ mRunFaceDetection.run();
+ }
+ }, mHandler);
+ }
+
+ private void onSaveClicked() {
+ // TODO this code needs to change to use the decode/crop/encode single
+ // step api so that we don't require that the whole (possibly large)
+ // bitmap doesn't have to be read into memory
+ if (mCrop == null) {
+ return;
+ }
+
+ if (mSaving) return;
+ mSaving = true;
+
+ Bitmap croppedImage;
+
+ // If the output is required to a specific size, create an new image
+ // with the cropped image in the center and the extra space filled.
+ if (mOutputX != 0 && mOutputY != 0 && !mScale) {
+ // Don't scale the image but instead fill it so it's the
+ // required dimension
+ croppedImage = Bitmap.createBitmap(mOutputX, mOutputY,
+ Bitmap.Config.RGB_565);
+ Canvas canvas = new Canvas(croppedImage);
+
+ Rect srcRect = mCrop.getCropRect();
+ Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);
+
+ int dx = (srcRect.width() - dstRect.width()) / 2;
+ int dy = (srcRect.height() - dstRect.height()) / 2;
+
+ // If the srcRect is too big, use the center part of it.
+ srcRect.inset(Math.max(0, dx), Math.max(0, dy));
+
+ // If the dstRect is too big, use the center part of it.
+ dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));
+
+ // Draw the cropped bitmap in the center
+ canvas.drawBitmap(mBitmap, srcRect, dstRect, null);
+
+ // Release bitmap memory as soon as possible
+ mImageView.clear();
+ mBitmap.recycle();
+ } else {
+ Rect r = mCrop.getCropRect();
+
+ int width = r.width();
+ int height = r.height();
+
+ // If we are circle cropping, we want alpha channel, which is the
+ // third param here.
+ croppedImage = Bitmap.createBitmap(width, height,
+ mCircleCrop
+ ? Bitmap.Config.ARGB_8888
+ : Bitmap.Config.RGB_565);
+
+ Canvas canvas = new Canvas(croppedImage);
+ Rect dstRect = new Rect(0, 0, width, height);
+ canvas.drawBitmap(mBitmap, r, dstRect, null);
+
+ // Release bitmap memory as soon as possible
+ mImageView.clear();
+ mBitmap.recycle();
+
+ if (mCircleCrop) {
+ // OK, so what's all this about?
+ // Bitmaps are inherently rectangular but we want to return
+ // something that's basically a circle. So we fill in the
+ // area around the circle with alpha. Note the all important
+ // PortDuff.Mode.CLEAR.
+ Canvas c = new Canvas(croppedImage);
+ Path p = new Path();
+ p.addCircle(width / 2F, height / 2F, width / 2F,
+ Path.Direction.CW);
+ c.clipPath(p, Region.Op.DIFFERENCE);
+ c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
+ }
+
+ // If the required dimension is specified, scale the image.
+ if (mOutputX != 0 && mOutputY != 0 && mScale) {
+ croppedImage = Util.transform(new Matrix(), croppedImage,
+ mOutputX, mOutputY, mScaleUp, Util.RECYCLE_INPUT);
+ }
+ }
+
+ mImageView.setImageBitmapResetBase(croppedImage, true);
+ mImageView.center(true, true);
+ mImageView.mHighlightViews.clear();
+
+ // Return the cropped image directly or save it to the specified URI.
+ Bundle myExtras = getIntent().getExtras();
+ if (myExtras != null && (myExtras.getParcelable("data") != null
+ || myExtras.getBoolean("return-data"))) {
+ Bundle extras = new Bundle();
+ extras.putParcelable("data", croppedImage);
+ setResult(RESULT_OK,
+ (new Intent()).setAction("inline-data").putExtras(extras));
+ finish();
+ } else {
+ final Bitmap b = croppedImage;
+ final int msdId = mSetWallpaper
+ ? R.string.wallpaper
+ : R.string.savingImage;
+ Util.startBackgroundJob(this, null,
+ getResources().getString(msdId),
+ new Runnable() {
+ public void run() {
+ saveOutput(b);
+ }
+ }, mHandler);
+ }
+ }
+
+ private void saveOutput(Bitmap croppedImage) {
+ if (mSaveUri != null) {
+ OutputStream outputStream = null;
+ try {
+ outputStream = mContentResolver.openOutputStream(mSaveUri);
+ if (outputStream != null) {
+ croppedImage.compress(mOutputFormat, 75, outputStream);
+ }
+ } catch (IOException ex) {
+ // TODO: report error to caller
+ Log.e(TAG, "Cannot open file: " + mSaveUri, ex);
+ } finally {
+ Util.closeSilently(outputStream);
+ }
+ Bundle extras = new Bundle();
+ setResult(RESULT_OK, new Intent(mSaveUri.toString())
+ .putExtras(extras));
+ } else if (mSetWallpaper) {
+ try {
+ WallpaperManager.getInstance(this).setBitmap(croppedImage);
+ setResult(RESULT_OK);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to set wallpaper.", e);
+ setResult(RESULT_CANCELED);
+ }
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("rect", mCrop.getCropRect().toString());
+
+ File oldPath = new File(mImage.getDataPath());
+ File directory = new File(oldPath.getParent());
+
+ int x = 0;
+ String fileName = oldPath.getName();
+ fileName = fileName.substring(0, fileName.lastIndexOf("."));
+
+ // Try file-1.jpg, file-2.jpg, ... until we find a filename which
+ // does not exist yet.
+ while (true) {
+ x += 1;
+ String candidate = directory.toString()
+ + "/" + fileName + "-" + x + ".jpg";
+ boolean exists = (new File(candidate)).exists();
+ if (!exists) {
+ break;
+ }
+ }
+
+ try {
+ int[] degree = new int[1];
+ Uri newUri = ImageManager.addImage(
+ mContentResolver,
+ mImage.getTitle(),
+ mImage.getDateTaken(),
+ null, // TODO this null is going to cause us to lose
+ // the location (gps).
+ directory.toString(), fileName + "-" + x + ".jpg",
+ croppedImage, null,
+ degree);
+
+ setResult(RESULT_OK, new Intent()
+ .setAction(newUri.toString())
+ .putExtras(extras));
+ } catch (Exception ex) {
+ // basically ignore this or put up
+ // some ui saying we failed
+ Log.e(TAG, "store image fail, continue anyway", ex);
+ }
+ }
+
+ final Bitmap b = croppedImage;
+ mHandler.post(new Runnable() {
+ public void run() {
+ mImageView.clear();
+ b.recycle();
+ }
+ });
+
+ finish();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mAllImages != null) {
+ mAllImages.close();
+ }
+ super.onDestroy();
+ }
+
+ Runnable mRunFaceDetection = new Runnable() {
+ @SuppressWarnings("hiding")
+ float mScale = 1F;
+ Matrix mImageMatrix;
+ FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
+ int mNumFaces;
+
+ // For each face, we create a HightlightView for it.
+ private void handleFace(FaceDetector.Face f) {
+ PointF midPoint = new PointF();
+
+ int r = ((int) (f.eyesDistance() * mScale)) * 2;
+ f.getMidPoint(midPoint);
+ midPoint.x *= mScale;
+ midPoint.y *= mScale;
+
+ int midX = (int) midPoint.x;
+ int midY = (int) midPoint.y;
+
+ HighlightView hv = new HighlightView(mImageView);
+
+ int width = mBitmap.getWidth();
+ int height = mBitmap.getHeight();
+
+ Rect imageRect = new Rect(0, 0, width, height);
+
+ RectF faceRect = new RectF(midX, midY, midX, midY);
+ faceRect.inset(-r, -r);
+ if (faceRect.left < 0) {
+ faceRect.inset(-faceRect.left, -faceRect.left);
+ }
+
+ if (faceRect.top < 0) {
+ faceRect.inset(-faceRect.top, -faceRect.top);
+ }
+
+ if (faceRect.right > imageRect.right) {
+ faceRect.inset(faceRect.right - imageRect.right,
+ faceRect.right - imageRect.right);
+ }
+
+ if (faceRect.bottom > imageRect.bottom) {
+ faceRect.inset(faceRect.bottom - imageRect.bottom,
+ faceRect.bottom - imageRect.bottom);
+ }
+
+ hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop,
+ mAspectX != 0 && mAspectY != 0);
+
+ mImageView.add(hv);
+ }
+
+ // Create a default HightlightView if we found no face in the picture.
+ private void makeDefault() {
+ HighlightView hv = new HighlightView(mImageView);
+
+ int width = mBitmap.getWidth();
+ int height = mBitmap.getHeight();
+
+ Rect imageRect = new Rect(0, 0, width, height);
+
+ // make the default size about 4/5 of the width or height
+ int cropWidth = Math.min(width, height) * 4 / 5;
+ int cropHeight = cropWidth;
+
+ if (mAspectX != 0 && mAspectY != 0) {
+ if (mAspectX > mAspectY) {
+ cropHeight = cropWidth * mAspectY / mAspectX;
+ } else {
+ cropWidth = cropHeight * mAspectX / mAspectY;
+ }
+ }
+
+ int x = (width - cropWidth) / 2;
+ int y = (height - cropHeight) / 2;
+
+ RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
+ hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop,
+ mAspectX != 0 && mAspectY != 0);
+ mImageView.add(hv);
+ }
+
+ // Scale the image down for faster face detection.
+ private Bitmap prepareBitmap() {
+ if (mBitmap == null) {
+ return null;
+ }
+
+ // 256 pixels wide is enough.
+ if (mBitmap.getWidth() > 256) {
+ mScale = 256.0F / mBitmap.getWidth();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(mScale, mScale);
+ Bitmap faceBitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap
+ .getWidth(), mBitmap.getHeight(), matrix, true);
+ return faceBitmap;
+ }
+
+ public void run() {
+ mImageMatrix = mImageView.getImageMatrix();
+ Bitmap faceBitmap = prepareBitmap();
+
+ mScale = 1.0F / mScale;
+ if (faceBitmap != null && mDoFaceDetection) {
+ FaceDetector detector = new FaceDetector(faceBitmap.getWidth(),
+ faceBitmap.getHeight(), mFaces.length);
+ mNumFaces = detector.findFaces(faceBitmap, mFaces);
+ }
+
+ if (faceBitmap != null && faceBitmap != mBitmap) {
+ faceBitmap.recycle();
+ }
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ mWaitingToPick = mNumFaces > 1;
+ if (mNumFaces > 0) {
+ for (int i = 0; i < mNumFaces; i++) {
+ handleFace(mFaces[i]);
+ }
+ } else {
+ makeDefault();
+ }
+ mImageView.invalidate();
+ if (mImageView.mHighlightViews.size() == 1) {
+ mCrop = mImageView.mHighlightViews.get(0);
+ mCrop.setFocus(true);
+ }
+
+ if (mNumFaces > 1) {
+ Toast t = Toast.makeText(CropImage.this,
+ R.string.multiface_crop_help,
+ Toast.LENGTH_SHORT);
+ t.show();
+ }
+ }
+ });
+ }
+ };
+}
+
+class CropImageView extends ImageViewTouchBase {
+ ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
+ HighlightView mMotionHighlightView = null;
+ float mLastX, mLastY;
+ int mMotionEdge;
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top,
+ int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mBitmapDisplayed.getBitmap() != null) {
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ if (hv.mIsFocused) {
+ centerBasedOnHighlightView(hv);
+ }
+ }
+ }
+ }
+
+ public CropImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void zoomTo(float scale, float centerX, float centerY) {
+ super.zoomTo(scale, centerX, centerY);
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ @Override
+ protected void zoomIn() {
+ super.zoomIn();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ @Override
+ protected void zoomOut() {
+ super.zoomOut();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ @Override
+ protected void postTranslate(float deltaX, float deltaY) {
+ super.postTranslate(deltaX, deltaY);
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ hv.mMatrix.postTranslate(deltaX, deltaY);
+ hv.invalidate();
+ }
+ }
+
+ // According to the event's position, change the focus to the first
+ // hitting cropping rectangle.
+ private void recomputeFocus(MotionEvent event) {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ hv.setFocus(false);
+ hv.invalidate();
+ }
+
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ int edge = hv.getHit(event.getX(), event.getY());
+ if (edge != HighlightView.GROW_NONE) {
+ if (!hv.hasFocus()) {
+ hv.setFocus(true);
+ hv.invalidate();
+ }
+ break;
+ }
+ }
+ invalidate();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ CropImage cropImage = (CropImage) mContext;
+ if (cropImage.mSaving) {
+ return false;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (cropImage.mWaitingToPick) {
+ recomputeFocus(event);
+ } else {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ int edge = hv.getHit(event.getX(), event.getY());
+ if (edge != HighlightView.GROW_NONE) {
+ mMotionEdge = edge;
+ mMotionHighlightView = hv;
+ mLastX = event.getX();
+ mLastY = event.getY();
+ mMotionHighlightView.setMode(
+ (edge == HighlightView.MOVE)
+ ? HighlightView.ModifyMode.Move
+ : HighlightView.ModifyMode.Grow);
+ break;
+ }
+ }
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ if (cropImage.mWaitingToPick) {
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ HighlightView hv = mHighlightViews.get(i);
+ if (hv.hasFocus()) {
+ cropImage.mCrop = hv;
+ for (int j = 0; j < mHighlightViews.size(); j++) {
+ if (j == i) {
+ continue;
+ }
+ mHighlightViews.get(j).setHidden(true);
+ }
+ centerBasedOnHighlightView(hv);
+ ((CropImage) mContext).mWaitingToPick = false;
+ return true;
+ }
+ }
+ } else if (mMotionHighlightView != null) {
+ centerBasedOnHighlightView(mMotionHighlightView);
+ mMotionHighlightView.setMode(
+ HighlightView.ModifyMode.None);
+ }
+ mMotionHighlightView = null;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (cropImage.mWaitingToPick) {
+ recomputeFocus(event);
+ } else if (mMotionHighlightView != null) {
+ mMotionHighlightView.handleMotion(mMotionEdge,
+ event.getX() - mLastX,
+ event.getY() - mLastY);
+ mLastX = event.getX();
+ mLastY = event.getY();
+
+ if (true) {
+ // This section of code is optional. It has some user
+ // benefit in that moving the crop rectangle against
+ // the edge of the screen causes scrolling but it means
+ // that the crop rectangle is no longer fixed under
+ // the user's finger.
+ ensureVisible(mMotionHighlightView);
+ }
+ }
+ break;
+ }
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_UP:
+ center(true, true);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ // if we're not zoomed then there's no point in even allowing
+ // the user to move the image around. This call to center puts
+ // it back to the normalized location (with false meaning don't
+ // animate).
+ if (getScale() == 1F) {
+ center(true, true);
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ // Pan the displayed image to make sure the cropping rectangle is visible.
+ private void ensureVisible(HighlightView hv) {
+ Rect r = hv.mDrawRect;
+
+ int panDeltaX1 = Math.max(0, mLeft - r.left);
+ int panDeltaX2 = Math.min(0, mRight - r.right);
+
+ int panDeltaY1 = Math.max(0, mTop - r.top);
+ int panDeltaY2 = Math.min(0, mBottom - r.bottom);
+
+ int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
+ int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
+
+ if (panDeltaX != 0 || panDeltaY != 0) {
+ panBy(panDeltaX, panDeltaY);
+ }
+ }
+
+ // If the cropping rectangle's size changed significantly, change the
+ // view's center and scale according to the cropping rectangle.
+ private void centerBasedOnHighlightView(HighlightView hv) {
+ Rect drawRect = hv.mDrawRect;
+
+ float width = drawRect.width();
+ float height = drawRect.height();
+
+ float thisWidth = getWidth();
+ float thisHeight = getHeight();
+
+ float z1 = thisWidth / width * .6F;
+ float z2 = thisHeight / height * .6F;
+
+ float zoom = Math.min(z1, z2);
+ zoom = zoom * this.getScale();
+ zoom = Math.max(1F, zoom);
+
+ if ((Math.abs(zoom - getScale()) / zoom) > .1) {
+ float [] coordinates = new float[] {hv.mCropRect.centerX(),
+ hv.mCropRect.centerY()};
+ getImageMatrix().mapPoints(coordinates);
+ zoomTo(zoom, coordinates[0], coordinates[1], 300F);
+ }
+
+ ensureVisible(hv);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ for (int i = 0; i < mHighlightViews.size(); i++) {
+ mHighlightViews.get(i).draw(canvas);
+ }
+ }
+
+ public void add(HighlightView hv) {
+ mHighlightViews.add(hv);
+ invalidate();
+ }
+}
View
412 src/com/android/camera/HighlightView.java
@@ -0,0 +1,412 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.camera;
+
+import com.android.gallery.R;
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+// This class is used by CropImage to display a highlighted cropping rectangle
+// overlayed with the image. There are two coordinate spaces in use. One is
+// image, another is screen. computeLayout() uses mMatrix to map from image
+// space to screen space.
+class HighlightView {
+
+ @SuppressWarnings("unused")
+ private static final String TAG = "HighlightView";
+ View mContext; // The View displaying the image.
+
+ public static final int GROW_NONE = (1 << 0);
+ public static final int GROW_LEFT_EDGE = (1 << 1);
+ public static final int GROW_RIGHT_EDGE = (1 << 2);
+ public static final int GROW_TOP_EDGE = (1 << 3);
+ public static final int GROW_BOTTOM_EDGE = (1 << 4);
+ public static final int MOVE = (1 << 5);
+
+ public HighlightView(View ctx) {
+ mContext = ctx;
+ }
+
+ private void init() {
+ android.content.res.Resources resources = mContext.getResources();
+ mResizeDrawableWidth =
+ resources.getDrawable(R.drawable.camera_crop_width);
+ mResizeDrawableHeight =
+ resources.getDrawable(R.drawable.camera_crop_height);
+ mResizeDrawableDiagonal =
+ resources.getDrawable(R.drawable.indicator_autocrop);
+ }
+
+ boolean mIsFocused;
+ boolean mHidden;
+
+ public boolean hasFocus() {
+ return mIsFocused;
+ }
+
+ public void setFocus(boolean f) {
+ mIsFocused = f;
+ }
+
+ public void setHidden(boolean hidden) {
+ mHidden = hidden;
+ }
+
+ protected void draw(Canvas canvas) {
+ if (mHidden) {
+ return;
+ }
+ canvas.save();
+ Path path = new Path();
+ if (!hasFocus()) {
+ mOutlinePaint.setColor(0xFF000000);
+ canvas.drawRect(mDrawRect, mOutlinePaint);
+ } else {
+ Rect viewDrawingRect = new Rect();
+ mContext.getDrawingRect(viewDrawingRect);
+ if (mCircle) {
+ float width = mDrawRect.width();
+ float height = mDrawRect.height();
+ path.addCircle(mDrawRect.left + (width / 2),
+ mDrawRect.top + (height / 2),
+ width / 2,
+ Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFEF04D6);
+ } else {
+ path.addRect(new RectF(mDrawRect), Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFFF8A00);
+ }
+ canvas.clipPath(path, Region.Op.DIFFERENCE);
+ canvas.drawRect(viewDrawingRect,
+ hasFocus() ? mFocusPaint : mNoFocusPaint);
+
+ canvas.restore();
+ canvas.drawPath(path, mOutlinePaint);
+
+ if (mMode == ModifyMode.Grow) {
+ if (mCircle) {
+ int width = mResizeDrawableDiagonal.getIntrinsicWidth();
+ int height = mResizeDrawableDiagonal.getIntrinsicHeight();
+
+ int d = (int) Math.round(Math.cos(/*45deg*/Math.PI / 4D)
+ * (mDrawRect.width() / 2D));
+ int x = mDrawRect.left
+ + (mDrawRect.width() / 2) + d - width / 2;
+ int y = mDrawRect.top
+ + (mDrawRect.height() / 2) - d - height / 2;
+ mResizeDrawableDiagonal.setBounds(x, y,
+ x + mResizeDrawableDiagonal.getIntrinsicWidth(),
+ y + mResizeDrawableDiagonal.getIntrinsicHeight());
+ mResizeDrawableDiagonal.draw(canvas);
+ } else {
+ int left = mDrawRect.left + 1;
+ int right = mDrawRect.right + 1;
+ int top = mDrawRect.top + 4;
+ int bottom = mDrawRect.bottom + 3;
+
+ int widthWidth =
+ mResizeDrawableWidth.getIntrinsicWidth() / 2;
+ int widthHeight =
+ mResizeDrawableWidth.getIntrinsicHeight() / 2;
+ int heightHeight =
+ mResizeDrawableHeight.getIntrinsicHeight() / 2;
+ int heightWidth =
+ mResizeDrawableHeight.getIntrinsicWidth() / 2;
+
+ int xMiddle = mDrawRect.left
+ + ((mDrawRect.right - mDrawRect.left) / 2);
+ int yMiddle = mDrawRect.top
+ + ((mDrawRect.bottom - mDrawRect.top) / 2);
+
+ mResizeDrawableWidth.setBounds(left - widthWidth,
+ yMiddle - widthHeight,
+ left + widthWidth,
+ yMiddle + widthHeight);
+ mResizeDrawableWidth.draw(canvas);
+
+ mResizeDrawableWidth.setBounds(right - widthWidth,
+ yMiddle - widthHeight,
+ right + widthWidth,
+ yMiddle + widthHeight);
+ mResizeDrawableWidth.draw(canvas);
+
+ mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
+ top - heightHeight,
+ xMiddle + heightWidth,
+ top + heightHeight);
+ mResizeDrawableHeight.draw(canvas);
+
+ mResizeDrawableHeight.setBounds(xMiddle - heightWidth,
+ bottom - heightHeight,
+ xMiddle + heightWidth,
+ bottom + heightHeight);
+ mResizeDrawableHeight.draw(canvas);
+ }
+ }
+ }
+ }
+
+ public void setMode(ModifyMode mode) {
+ if (mode != mMode) {
+ mMode = mode;
+ mContext.invalidate();
+ }
+ }
+
+ // Determines which edges are hit by touching at (x, y).
+ public int getHit(float x, float y) {
+ Rect r = computeLayout();
+ final float hysteresis = 20F;
+ int retval = GROW_NONE;
+
+ if (mCircle) {
+ float distX = x - r.centerX();
+ float distY = y - r.centerY();
+ int distanceFromCenter =
+ (int) Math.sqrt(distX * distX + distY * distY);
+ int radius = mDrawRect.width() / 2;
+ int delta = distanceFromCenter - radius;
+ if (Math.abs(delta) <= hysteresis) {
+ if (Math.abs(distY) > Math.abs(distX)) {
+ if (distY < 0) {
+ retval = GROW_TOP_EDGE;
+ } else {
+ retval = GROW_BOTTOM_EDGE;
+ }
+ } else {
+ if (distX < 0) {
+ retval = GROW_LEFT_EDGE;
+ } else {
+ retval = GROW_RIGHT_EDGE;
+ }
+ }
+ } else if (distanceFromCenter < radius) {
+ retval = MOVE;
+ } else {
+ retval = GROW_NONE;
+ }
+ } else {
+ // verticalCheck makes sure the position is between the top and
+ // the bottom edge (with some tolerance). Similar for horizCheck.
+ boolean verticalCheck = (y >= r.top - hysteresis)
+ && (y < r.bottom + hysteresis);
+ boolean horizCheck = (x >= r.left - hysteresis)
+ && (x < r.right + hysteresis);
+
+ // Check whether the position is near some edge(s).
+ if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
+ retval |= GROW_LEFT_EDGE;
+ }
+ if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
+ retval |= GROW_RIGHT_EDGE;
+ }
+ if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
+ retval |= GROW_TOP_EDGE;
+ }
+ if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
+ retval |= GROW_BOTTOM_EDGE;
+ }
+
+ // Not near any edge but inside the rectangle: move.
+ if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
+ retval = MOVE;
+ }
+ }
+ return retval;
+ }
+
+ // Handles motion (dx, dy) in screen space.
+ // The "edge" parameter specifies which edges the user is dragging.
+ void handleMotion(int edge, float dx, float dy) {
+ Rect r = computeLayout();
+ if (edge == GROW_NONE) {
+ return;
+ } else if (edge == MOVE) {
+ // Convert to image space before sending to moveBy().
+ moveBy(dx * (mCropRect.width() / r.width()),
+ dy * (mCropRect.height() / r.height()));
+ } else {
+ if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
+ dx = 0;
+ }
+
+ if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
+ dy = 0;
+ }
+
+ // Convert to image space before sending to growBy().
+ float xDelta = dx * (mCropRect.width() / r.width());
+ float yDelta = dy * (mCropRect.height() / r.height());
+ growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
+ (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
+ }
+ }
+
+ // Grows the cropping rectange by (dx, dy) in image space.
+ void moveBy(float dx, float dy) {
+ Rect invalRect = new Rect(mDrawRect);
+
+ mCropRect.offset(dx, dy);
+
+ // Put the cropping rectangle inside image rectangle.
+ mCropRect.offset(
+ Math.max(0, mImageRect.left - mCropRect.left),
+ Math.max(0, mImageRect.top - mCropRect.top));
+
+ mCropRect.offset(
+ Math.min(0, mImageRect.right - mCropRect.right),
+ Math.min(0, mImageRect.bottom - mCropRect.bottom));
+
+ mDrawRect = computeLayout();
+ invalRect.union(mDrawRect);
+ invalRect.inset(-10, -10);
+ mContext.invalidate(invalRect);
+ }
+
+ // Grows the cropping rectange by (dx, dy) in image space.
+ void growBy(float dx, float dy) {
+ if (mMaintainAspectRatio) {
+ if (dx != 0) {
+ dy = dx / mInitialAspectRatio;
+ } else if (dy != 0) {
+ dx = dy * mInitialAspectRatio;
+ }
+ }
+
+ // Don't let the cropping rectangle grow too fast.
+ // Grow at most half of the difference between the image rectangle and
+ // the cropping rectangle.
+ RectF r = new RectF(mCropRect);
+ if (dx > 0F && r.width() + 2 * dx > mImageRect.width()) {
+ float adjustment = (mImageRect.width() - r.width()) / 2F;
+ dx = adjustment;
+ if (mMaintainAspectRatio) {
+ dy = dx / mInitialAspectRatio;
+ }
+ }
+ if (dy > 0F && r.height() + 2 * dy > mImageRect.height()) {
+ float adjustment = (mImageRect.height() - r.height()) / 2F;
+ dy = adjustment;
+ if (mMaintainAspectRatio) {
+ dx = dy * mInitialAspectRatio;
+ }
+ }
+
+ r.inset(-dx, -dy);
+
+ // Don't let the cropping rectangle shrink too fast.
+ final float widthCap = 25F;
+ if (r.width() < widthCap) {
+ r.inset(-(widthCap - r.width()) / 2F, 0F);
+ }
+ float heightCap = mMaintainAspectRatio
+ ? (widthCap / mInitialAspectRatio)
+ : widthCap;
+ if (r.height() < heightCap) {
+ r.inset(0F, -(heightCap - r.height()) / 2F);
+ }
+
+ // Put the cropping rectangle inside the image rectangle.
+ if (r.left < mImageRect.left) {
+ r.offset(mImageRect.left - r.left, 0F);
+ } else if (r.right > mImageRect.right) {
+ r.offset(-(r.right - mImageRect.right), 0);
+ }
+ if (r.top < mImageRect.top) {
+ r.offset(0F, mImageRect.top - r.top);
+ } else if (r.bottom > mImageRect.bottom) {
+ r.offset(0F, -(r.bottom - mImageRect.bottom));
+ }
+
+ mCropRect.set(r);
+ mDrawRect = computeLayout();
+ mContext.invalidate();
+ }
+
+ // Returns the cropping rectangle in image space.
+ public Rect getCropRect() {
+ return new Rect((int) mCropRect.left, (int) mCropRect.top,
+ (int) mCropRect.right, (int) mCropRect.bottom);
+ }
+
+ // Maps the cropping rectangle from image space to screen space.
+ private Rect computeLayout() {
+ RectF r = new RectF(mCropRect.left, mCropRect.top,
+ mCropRect.right, mCropRect.bottom);
+ mMatrix.mapRect(r);
+ return new Rect(Math.round(r.left), Math.round(r.top),
+ Math.round(r.right), Math.round(r.bottom));
+ }
+
+ public void invalidate() {
+ mDrawRect = computeLayout();
+ }
+
+ public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean circle,
+ boolean maintainAspectRatio) {
+ if (circle) {
+ maintainAspectRatio = true;
+ }
+ mMatrix = new Matrix(m);
+
+ mCropRect = cropRect;
+ mImageRect = new RectF(imageRect);
+ mMaintainAspectRatio = maintainAspectRatio;
+ mCircle = circle;
+
+ mInitialAspectRatio = mCropRect.width() / mCropRect.height();
+ mDrawRect = computeLayout();
+
+ mFocusPaint.setARGB(125, 50, 50, 50);
+ mNoFocusPaint.setARGB(125, 50, 50, 50);
+ mOutlinePaint.setStrokeWidth(3F);
+ mOutlinePaint.setStyle(Paint.Style.STROKE);
+ mOutlinePaint.setAntiAlias(true);
+
+ mMode = ModifyMode.None;
+ init();
+ }
+
+ enum ModifyMode { None, Move, Grow }
+
+ private ModifyMode mMode = ModifyMode.None;
+
+ Rect mDrawRect; // in screen space
+ private RectF mImageRect; // in image space
+ RectF mCropRect; // in image space
+ Matrix mMatrix;
+
+ private boolean mMaintainAspectRatio = false;
+ private float mInitialAspectRatio;
+ private boolean mCircle = false;
+
+ private Drawable mResizeDrawableWidth;
+ private Drawable mResizeDrawableHeight;
+ private Drawable mResizeDrawableDiagonal;
+
+ private final Paint mFocusPaint = new Paint();
+ private final Paint mNoFocusPaint = new Paint();
+ private final Paint mOutlinePaint = new Paint();
+}
View
407 src/com/android/camera/ImageViewTouchBase.java
@@ -0,0 +1,407 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,