summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
committerThe Android Open Source Project <initial-contribution@android.com>2008-10-21 07:00:00 -0700
commit1d4c75065966c4f6f56900e31f655bfd1b334435 (patch)
tree5d4526db3153daa63087fcb9384f8bc0659fbd18
downloadLegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.zip
LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.gz
LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.bz2
Initial Contribution
-rw-r--r--Android.mk16
-rw-r--r--AndroidManifest.xml207
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE190
-rw-r--r--res/anim/alpha_in.xml24
-rw-r--r--res/anim/alpha_out.xml25
-rw-r--r--res/anim/auto_focus_blink.xml19
-rw-r--r--res/anim/hyperspace_out.xml56
-rw-r--r--res/anim/rotate_in.xml24
-rw-r--r--res/anim/rotate_out.xml26
-rw-r--r--res/anim/rotate_shrink_in.xml38
-rw-r--r--res/anim/rotate_shrink_out.xml38
-rw-r--r--res/anim/slide_in.xml21
-rw-r--r--res/anim/slide_in_vertical.xml21
-rw-r--r--res/anim/slide_out.xml21
-rw-r--r--res/anim/slide_out_vertical.xml21
-rw-r--r--res/anim/transition_in.xml24
-rw-r--r--res/anim/transition_out.xml25
-rw-r--r--res/drawable/app_camera.pngbin0 -> 3235 bytes
-rw-r--r--res/drawable/app_photos.pngbin0 -> 2665 bytes
-rw-r--r--res/drawable/btn_camera_arrow_left.xml28
-rw-r--r--res/drawable/btn_camera_arrow_left_default.pngbin0 -> 2217 bytes
-rw-r--r--res/drawable/btn_camera_arrow_left_press.pngbin0 -> 1895 bytes
-rw-r--r--res/drawable/btn_camera_arrow_right.xml28
-rw-r--r--res/drawable/btn_camera_arrow_right_default.pngbin0 -> 2168 bytes
-rw-r--r--res/drawable/btn_camera_arrow_right_press.pngbin0 -> 1947 bytes
-rwxr-xr-xres/drawable/camera_crop_height.pngbin0 -> 2100 bytes
-rwxr-xr-xres/drawable/camera_crop_width.pngbin0 -> 2085 bytes
-rw-r--r--res/drawable/carousel_tray_bg.9.pngbin0 -> 3030 bytes
-rw-r--r--res/drawable/counter_divider.pngbin0 -> 3013 bytes
-rw-r--r--res/drawable/delete_image.pngbin0 -> 3216 bytes
-rw-r--r--res/drawable/frame_gallery_preview.xml31
-rwxr-xr-xres/drawable/frame_gallery_preview_album.pngbin0 -> 3158 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_mask.pngbin0 -> 624 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_pressed.pngbin0 -> 3122 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_selected.pngbin0 -> 2432 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_sublevel.pngbin0 -> 3226 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_sublevel_mask.pngbin0 -> 612 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_sublevel_pressed.pngbin0 -> 2586 bytes
-rwxr-xr-xres/drawable/frame_gallery_preview_album_sublevel_selected.pngbin0 -> 2447 bytes
-rwxr-xr-xres/drawable/frame_gallery_thumb_uploaded_overlay.pngbin0 -> 678 bytes
-rwxr-xr-xres/drawable/frame_overlay_gallery_camera.pngbin0 -> 1833 bytes
-rwxr-xr-xres/drawable/frame_overlay_gallery_downloaded.pngbin0 -> 1541 bytes
-rwxr-xr-xres/drawable/frame_overlay_gallery_video.pngbin0 -> 1919 bytes
-rw-r--r--res/drawable/grid_background.xml24
-rw-r--r--res/drawable/highlight_unfocus.9.pngbin0 -> 1054 bytes
-rwxr-xr-xres/drawable/ic_camera_flash.pngbin0 -> 3010 bytes
-rw-r--r--res/drawable/ic_camera_indicator_auto_focus_green.pngbin0 -> 624 bytes
-rwxr-xr-xres/drawable/ic_camera_indicator_flash_auto.pngbin0 -> 1324 bytes
-rwxr-xr-xres/drawable/ic_camera_indicator_flash_off.pngbin0 -> 1654 bytes
-rwxr-xr-xres/drawable/ic_camera_indicator_flash_on.pngbin0 -> 1214 bytes
-rw-r--r--res/drawable/ic_camera_indicator_memory.9.pngbin0 -> 3343 bytes
-rwxr-xr-xres/drawable/ic_camera_indicator_photo.pngbin0 -> 1176 bytes
-rwxr-xr-xres/drawable/ic_camera_indicator_video.pngbin0 -> 1862 bytes
-rwxr-xr-xres/drawable/ic_camera_select_image.pngbin0 -> 2863 bytes
-rwxr-xr-xres/drawable/ic_camera_select_video.pngbin0 -> 2910 bytes
-rwxr-xr-xres/drawable/ic_gallery_back_to_entry.pngbin0 -> 3094 bytes
-rwxr-xr-xres/drawable/ic_gallery_empty.pngbin0 -> 1849 bytes
-rwxr-xr-xres/drawable/ic_gallery_finish.pngbin0 -> 3107 bytes
-rwxr-xr-xres/drawable/ic_gallery_picasa_upload.pngbin0 -> 1114 bytes
-rwxr-xr-xres/drawable/ic_gallery_upload_to.pngbin0 -> 2954 bytes
-rwxr-xr-xres/drawable/ic_gallery_youtube_upload.pngbin0 -> 1262 bytes
-rwxr-xr-xres/drawable/ic_launcher_camera.pngbin0 -> 2880 bytes
-rwxr-xr-xres/drawable/ic_launcher_gallery.pngbin0 -> 3040 bytes
-rw-r--r--res/drawable/ic_menu_view_details.pngbin0 -> 2096 bytes
-rwxr-xr-xres/drawable/ic_video_empty.pngbin0 -> 1035 bytes
-rw-r--r--res/drawable/image_border_bg_focus_blue.9.pngbin0 -> 3618 bytes
-rw-r--r--res/drawable/image_border_bg_focus_blue9.pngbin0 -> 3618 bytes
-rw-r--r--res/drawable/image_border_bg_normal.9.pngbin0 -> 3423 bytes
-rw-r--r--res/drawable/image_border_bg_normal9.pngbin0 -> 3423 bytes
-rw-r--r--res/drawable/image_border_bg_pressed_blue.9.pngbin0 -> 3418 bytes
-rw-r--r--res/drawable/image_border_bg_pressed_blue9.pngbin0 -> 3418 bytes
-rw-r--r--res/drawable/indicator_autocrop.pngbin0 -> 3225 bytes
-rw-r--r--res/drawable/play_video.pngbin0 -> 1826 bytes
-rwxr-xr-xres/drawable/popup_camera_toast.9.pngbin0 -> 2563 bytes
-rw-r--r--res/layout/camera.xml118
-rw-r--r--res/layout/cropimage.xml58
-rw-r--r--res/layout/custom_gallery_title.xml61
-rw-r--r--res/layout/details.xml159
-rw-r--r--res/layout/detailsview.xml96
-rw-r--r--res/layout/gallery.xml58
-rw-r--r--res/layout/gallery_picker_item.xml43
-rw-r--r--res/layout/gallerypicker.xml32
-rw-r--r--res/layout/gallerysettings.xml63
-rw-r--r--res/layout/grid.xml44
-rw-r--r--res/layout/image_gallery.xml44
-rw-r--r--res/layout/image_gallery_2.xml58
-rw-r--r--res/layout/slide_show.xml27
-rw-r--r--res/layout/viewimage.xml96
-rw-r--r--res/layout/viewvideo.xml28
-rw-r--r--res/layout/youtube_upload_info.xml82
-rw-r--r--res/raw/camera_click.oggbin0 -> 5862 bytes
-rw-r--r--res/values-cs/strings.xml206
-rw-r--r--res/values-de-rDE/strings.xml229
-rw-r--r--res/values-es-rUS/strings.xml229
-rw-r--r--res/values-fr-rFR/strings.xml229
-rw-r--r--res/values-it-rIT/strings.xml229
-rw-r--r--res/values-nl-rNL/strings.xml206
-rw-r--r--res/values-zh-rTW/strings.xml229
-rw-r--r--res/values/ids.xml33
-rwxr-xr-xres/values/strings.xml368
-rw-r--r--res/xml/camera_preferences.xml53
-rw-r--r--res/xml/gallery_preferences.xml89
-rw-r--r--src/com/android/camera/BufferedInputStream.java231
-rw-r--r--src/com/android/camera/Camera.java1527
-rw-r--r--src/com/android/camera/CameraButtonIntentReceiver.java43
-rw-r--r--src/com/android/camera/CameraSettings.java65
-rw-r--r--src/com/android/camera/CameraThread.java95
-rw-r--r--src/com/android/camera/CropImage.java794
-rw-r--r--src/com/android/camera/DrmWallpaper.java35
-rw-r--r--src/com/android/camera/ErrorScreen.java95
-rw-r--r--src/com/android/camera/ExifInterface.java263
-rw-r--r--src/com/android/camera/GalleryPicker.java607
-rw-r--r--src/com/android/camera/GalleryPickerItem.java98
-rw-r--r--src/com/android/camera/GallerySettings.java46
-rw-r--r--src/com/android/camera/HighlightView.java428
-rw-r--r--src/com/android/camera/ImageGallery2.java1734
-rw-r--r--src/com/android/camera/ImageLoader.java348
-rwxr-xr-xsrc/com/android/camera/ImageManager.java3632
-rw-r--r--src/com/android/camera/ImageViewTouchBase.java547
-rw-r--r--src/com/android/camera/MenuHelper.java521
-rw-r--r--src/com/android/camera/PickWallpaper.java23
-rw-r--r--src/com/android/camera/PwaUpload.java56
-rw-r--r--src/com/android/camera/SelectedImageGetter.java25
-rw-r--r--src/com/android/camera/SlideShow.java425
-rw-r--r--src/com/android/camera/UploadAction.java34
-rw-r--r--src/com/android/camera/UploadService.java1181
-rw-r--r--src/com/android/camera/ViewImage.java1434
-rw-r--r--src/com/android/camera/ViewVideo.java210
-rw-r--r--src/com/android/camera/Wallpaper.java193
-rw-r--r--src/com/android/camera/YouTubeUpload.java145
-rw-r--r--tests/Android.mk18
-rw-r--r--tests/AndroidManifest.xml29
-rw-r--r--tests/src/com/android/camera/CameraLaunchPerformance.java53
134 files changed, 19009 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..d35d7cb
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,16 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := user development
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := Camera
+LOCAL_CERTIFICATE := media
+
+LOCAL_STATIC_JAVA_LIBRARIES := googlelogin-client
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..cd71194
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,207 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.camera"
+ android:sharedUserId="android.media">
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.lh2"/>
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.youtube"/>
+ <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.YouTubeUser"/>
+ <uses-permission android:name="android.permission.SET_WALLPAPER" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+ <application android:icon="@drawable/ic_launcher_camera"
+ android:label="@string/camera_label"
+ android:taskAffinity="">
+ <service android:name="UploadService" android:process="android.process.media" />
+ <receiver android:name="CameraButtonIntentReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.CAMERA_BUTTON"/>
+ </intent-filter>
+ </receiver>
+ <activity android:name="Camera"
+ android:configChanges="orientation|keyboardHidden"
+ android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
+ android:screenOrientation="landscape"
+ android:clearTaskOnLaunch="true"
+ android:taskAffinity="android.task.camera">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.media.action.IMAGE_CAPTURE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="GalleryPicker" android:label="@string/gallery_picker_label"
+ android:configChanges="orientation|keyboardHidden"
+ android:icon="@drawable/ic_launcher_gallery"
+ android:taskAffinity="android.task.pictures">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="ImageGallery2" android:label="@string/gallery_label"
+ android:configChanges="orientation|keyboardHidden"
+ android:icon="@drawable/ic_launcher_gallery">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.cursor.dir/image" />
+ </intent-filter>
+
+ <intent-filter>
+ <action android:name="android.intent.action.GET_CONTENT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <data android:mimeType="vnd.android.cursor.dir/image" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.GET_CONTENT" />
+ <category android:name="android.intent.category.OPENABLE" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PICK" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.PICK" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="vnd.android.cursor.dir/image" />
+ </intent-filter>
+
+ </activity>
+ <activity
+ android:name="CropImage"
+ android:process=":CropImage"
+ android:configChanges="orientation|keyboardHidden"
+ android:label="@string/crop_label">
+ <intent-filter android:label="@string/crop_label">
+ <action android:name="com.android.camera.action.CROP" />
+ <data android:mimeType="image/*" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.ALTERNATIVE" />
+ <category android:name="android.intent.category.SELECTED_ALTERNATIVE" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="ViewImage"
+ android:label="@string/view_label"
+ android:configChanges="orientation|keyboardHidden">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="image/*" />
+ </intent-filter>
+ </activity>
+
+<!--
+ <activity android:name="ViewVideo" android:label="@string/view_video_label">
+ android:configChanges="orientation|keyboardHidden"
+ android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:mimeType="video/*" />
+ </intent-filter>
+ </activity>
+-->
+
+ <activity android:name="CameraSettings" android:label="@string/preferences_label">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEVELOPMENT_PREFERENCE" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="GallerySettings" android:label="@string/preferences_label">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEVELOPMENT_PREFERENCE" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="SlideShow" android:label="Raw Image Viewer"
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.TEST" />
+ </intent-filter>
+ </activity>
+
+<!--
+ <activity android:name="PwaUpload" android:label="@string/picasa_upload_label">
+ <intent-filter>
+ <action android:name="android.intent.action.SEND" />
+ <data android:mimeType="image/*" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ </intent-filter>
+ </activity>
+-->
+ <activity android:name=".Wallpaper"
+ android:label="@string/camera_setas_wallpaper"
+ android:icon="@drawable/ic_launcher_gallery">
+ <intent-filter>
+ <action android:name="android.intent.action.ATTACH_DATA" />
+ <data android:mimeType="image/*" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name=".PickWallpaper"
+ android:label="@string/camera_pick_wallpaper"
+ android:icon="@drawable/ic_launcher_gallery">
+ <intent-filter>
+ <action android:name="android.intent.action.SET_WALLPAPER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <!--<activity android:name=".DrmWallpaper"-->
+ <!--android:label="@string/camera_setas_wallpaper_drm"-->
+ <!--android:icon="@drawable/ic_launcher_gallery">-->
+ <!--<intent-filter>-->
+ <!--<action android:name="android.intent.action.SET_WALLPAPER" />-->
+ <!--<category android:name="android.intent.category.DEFAULT" />-->
+ <!--</intent-filter>-->
+ <!--</activity>-->
+
+<!--
+ <activity android:name="YouTubeUpload" android:label="@string/youtube_upload_label">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="YouTubeUpload"
+ android:label="YouTubeUpload"
+ android:theme="@android:style/Theme.Light">
+
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity android:name="ErrorScreen" android:label="@string/error_label">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ </intent-filter>
+ </activity>
+-->
+ </application>
+</manifest>
+
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..c5b1efa
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-2008, 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.
+
+ 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.
+
+
+ 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
+
diff --git a/res/anim/alpha_in.xml b/res/anim/alpha_in.xml
new file mode 100644
index 0000000..6ad315a
--- /dev/null
+++ b/res/anim/alpha_in.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false">
+ <alpha
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"
+ android:fillAfter="true"
+ android:startOffset="900"
+ android:duration="700" />
+</set>
diff --git a/res/anim/alpha_out.xml b/res/anim/alpha_out.xml
new file mode 100644
index 0000000..bf6ee89
--- /dev/null
+++ b/res/anim/alpha_out.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false">
+ <alpha
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:fillAfter="true"
+ android:duration="700" />
+</set>
+
+
diff --git a/res/anim/auto_focus_blink.xml b/res/anim/auto_focus_blink.xml
new file mode 100644
index 0000000..dff217d
--- /dev/null
+++ b/res/anim/auto_focus_blink.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="200" />
diff --git a/res/anim/hyperspace_out.xml b/res/anim/hyperspace_out.xml
new file mode 100644
index 0000000..f963156
--- /dev/null
+++ b/res/anim/hyperspace_out.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false">
+
+ <scale
+ android:interpolator="@android:anim/accelerate_decelerate_interpolator"
+ android:fromXScale="1.0"
+ android:toXScale="1.4"
+ android:fromYScale="1.0"
+ android:toYScale="0.6"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fillAfter="false"
+ android:duration="700" />
+
+
+ <set
+ android:interpolator="@android:anim/accelerate_interpolator">
+
+ <scale
+ android:fromXScale="1.4"
+ android:toXScale="0.0"
+ android:fromYScale="0.6"
+ android:toYScale="0.0"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:startOffset="700"
+ android:duration="400"
+ android:fillBefore="false" />
+
+ <rotate
+ android:fromDegrees="0"
+ android:toDegrees="-45"
+ android:toYScale="0.0"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:startOffset="700"
+ android:duration="400" />
+ </set>
+
+</set>
+
diff --git a/res/anim/rotate_in.xml b/res/anim/rotate_in.xml
new file mode 100644
index 0000000..25edc8b
--- /dev/null
+++ b/res/anim/rotate_in.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromDegrees="90"
+ android:toDegrees="0"
+ android:toYScale="0.0"
+ android:pivotX="0%"
+ android:pivotY="100%"
+ android:startOffset="400"
+ android:duration="300" />
diff --git a/res/anim/rotate_out.xml b/res/anim/rotate_out.xml
new file mode 100644
index 0000000..4ebf33f
--- /dev/null
+++ b/res/anim/rotate_out.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromDegrees="0"
+ android:toDegrees="-90"
+ android:toYScale="0.0"
+ android:pivotX="0%"
+ android:pivotY="100%"
+ android:startOffset="0"
+ android:duration="300" />
+
+
diff --git a/res/anim/rotate_shrink_in.xml b/res/anim/rotate_shrink_in.xml
new file mode 100644
index 0000000..3582102
--- /dev/null
+++ b/res/anim/rotate_shrink_in.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false">
+ <scale
+ android:fromXScale="0.01"
+ android:toXScale="1.0"
+ android:fromYScale="0.01"
+ android:toYScale="1.0"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fillAfter="true"
+ android:startOffset="400"
+ android:duration="300" />
+
+ <rotate
+ android:fromDegrees="90"
+ android:toDegrees="0"
+ android:toYScale="0.0"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fillAfter="true"
+ android:startOffset="400"
+ android:duration="300" />
+</set>
diff --git a/res/anim/rotate_shrink_out.xml b/res/anim/rotate_shrink_out.xml
new file mode 100644
index 0000000..c608a8f
--- /dev/null
+++ b/res/anim/rotate_shrink_out.xml
@@ -0,0 +1,38 @@
+<?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.
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:shareInterpolator="false">
+ <scale
+ android:fromXScale="1.0"
+ android:toXScale="0.01"
+ android:fromYScale="1.0"
+ android:toYScale="0.01"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fillAfter="true"
+ android:duration="300" />
+
+ <rotate
+ android:fromDegrees="0"
+ android:toDegrees="-90"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:fillAfter="true"
+ android:startOffset="0"
+ android:duration="300" />
+</set>
+
+
diff --git a/res/anim/slide_in.xml b/res/anim/slide_in.xml
new file mode 100644
index 0000000..d1b9e5e
--- /dev/null
+++ b/res/anim/slide_in.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromXDelta="100%p"
+ android:toXDelta="0"
+ android:startOffset="0"
+ android:duration="400" />
diff --git a/res/anim/slide_in_vertical.xml b/res/anim/slide_in_vertical.xml
new file mode 100644
index 0000000..4727689
--- /dev/null
+++ b/res/anim/slide_in_vertical.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"
+ android:startOffset="0"
+ android:duration="400" />
diff --git a/res/anim/slide_out.xml b/res/anim/slide_out.xml
new file mode 100644
index 0000000..204cf28
--- /dev/null
+++ b/res/anim/slide_out.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromXDelta="0"
+ android:toXDelta="-100%p"
+ android:startOffset="0"
+ android:duration="400" />
diff --git a/res/anim/slide_out_vertical.xml b/res/anim/slide_out_vertical.xml
new file mode 100644
index 0000000..3f3f2a0
--- /dev/null
+++ b/res/anim/slide_out_vertical.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromYDelta="0"
+ android:toYDelta="-100%p"
+ android:startOffset="0"
+ android:duration="400" />
diff --git a/res/anim/transition_in.xml b/res/anim/transition_in.xml
new file mode 100644
index 0000000..6c2ad61
--- /dev/null
+++ b/res/anim/transition_in.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/res/anim/fade_in.xml
+**
+** Copyright 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.
+*/
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0"
+ android:duration="1000" />
diff --git a/res/anim/transition_out.xml b/res/anim/transition_out.xml
new file mode 100644
index 0000000..76e67f4
--- /dev/null
+++ b/res/anim/transition_out.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/res/anim/fade_out.xml
+**
+** Copyright 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.
+*/
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0"
+ android:duration="1000"
+/>
diff --git a/res/drawable/app_camera.png b/res/drawable/app_camera.png
new file mode 100644
index 0000000..a2bb888
--- /dev/null
+++ b/res/drawable/app_camera.png
Binary files differ
diff --git a/res/drawable/app_photos.png b/res/drawable/app_photos.png
new file mode 100644
index 0000000..971a41e
--- /dev/null
+++ b/res/drawable/app_photos.png
Binary files differ
diff --git a/res/drawable/btn_camera_arrow_left.xml b/res/drawable/btn_camera_arrow_left.xml
new file mode 100644
index 0000000..9d7dcdd
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_left.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/btn_camera_arrow_left_default" />
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_camera_arrow_left_press" />
+
+ <item
+ android:drawable="@drawable/btn_camera_arrow_left_default" />
+
+</selector>
diff --git a/res/drawable/btn_camera_arrow_left_default.png b/res/drawable/btn_camera_arrow_left_default.png
new file mode 100644
index 0000000..a9709aa
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_left_default.png
Binary files differ
diff --git a/res/drawable/btn_camera_arrow_left_press.png b/res/drawable/btn_camera_arrow_left_press.png
new file mode 100644
index 0000000..262c928
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_left_press.png
Binary files differ
diff --git a/res/drawable/btn_camera_arrow_right.xml b/res/drawable/btn_camera_arrow_right.xml
new file mode 100644
index 0000000..cc6ee2d
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_right.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/btn_camera_arrow_right_default" />
+
+ <item android:state_pressed="true"
+ android:drawable="@drawable/btn_camera_arrow_right_press" />
+
+ <item
+ android:drawable="@drawable/btn_camera_arrow_right_default" />
+
+</selector>
diff --git a/res/drawable/btn_camera_arrow_right_default.png b/res/drawable/btn_camera_arrow_right_default.png
new file mode 100644
index 0000000..a4f78dc
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_right_default.png
Binary files differ
diff --git a/res/drawable/btn_camera_arrow_right_press.png b/res/drawable/btn_camera_arrow_right_press.png
new file mode 100644
index 0000000..4b37b0d
--- /dev/null
+++ b/res/drawable/btn_camera_arrow_right_press.png
Binary files differ
diff --git a/res/drawable/camera_crop_height.png b/res/drawable/camera_crop_height.png
new file mode 100755
index 0000000..b089aec
--- /dev/null
+++ b/res/drawable/camera_crop_height.png
Binary files differ
diff --git a/res/drawable/camera_crop_width.png b/res/drawable/camera_crop_width.png
new file mode 100755
index 0000000..65216af
--- /dev/null
+++ b/res/drawable/camera_crop_width.png
Binary files differ
diff --git a/res/drawable/carousel_tray_bg.9.png b/res/drawable/carousel_tray_bg.9.png
new file mode 100644
index 0000000..d51babe
--- /dev/null
+++ b/res/drawable/carousel_tray_bg.9.png
Binary files differ
diff --git a/res/drawable/counter_divider.png b/res/drawable/counter_divider.png
new file mode 100644
index 0000000..ff35a08
--- /dev/null
+++ b/res/drawable/counter_divider.png
Binary files differ
diff --git a/res/drawable/delete_image.png b/res/drawable/delete_image.png
new file mode 100644
index 0000000..032e683
--- /dev/null
+++ b/res/drawable/delete_image.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview.xml b/res/drawable/frame_gallery_preview.xml
new file mode 100644
index 0000000..5550bff
--- /dev/null
+++ b/res/drawable/frame_gallery_preview.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_window_focused="false"
+ android:drawable="@drawable/frame_gallery_preview_album" />
+
+ <item android:state_selected="true" android:state_pressed="true"
+ android:drawable="@drawable/frame_gallery_preview_album_pressed" />
+
+ <item android:state_selected="false" android:state_pressed="true"
+ android:drawable="@drawable/frame_gallery_preview_album_pressed" />
+
+ <item android:state_selected="true"
+ android:drawable="@drawable/frame_gallery_preview_album_selected" />
+
+</selector>
diff --git a/res/drawable/frame_gallery_preview_album.png b/res/drawable/frame_gallery_preview_album.png
new file mode 100755
index 0000000..5b92ee2
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_mask.png b/res/drawable/frame_gallery_preview_album_mask.png
new file mode 100755
index 0000000..b053f30
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_mask.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_pressed.png b/res/drawable/frame_gallery_preview_album_pressed.png
new file mode 100755
index 0000000..e3575ff
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_pressed.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_selected.png b/res/drawable/frame_gallery_preview_album_selected.png
new file mode 100755
index 0000000..222a38a
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_selected.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_sublevel.png b/res/drawable/frame_gallery_preview_album_sublevel.png
new file mode 100755
index 0000000..489a4f4
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_sublevel.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_sublevel_mask.png b/res/drawable/frame_gallery_preview_album_sublevel_mask.png
new file mode 100755
index 0000000..4eefa03
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_sublevel_mask.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_sublevel_pressed.png b/res/drawable/frame_gallery_preview_album_sublevel_pressed.png
new file mode 100755
index 0000000..970993f
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_sublevel_pressed.png
Binary files differ
diff --git a/res/drawable/frame_gallery_preview_album_sublevel_selected.png b/res/drawable/frame_gallery_preview_album_sublevel_selected.png
new file mode 100755
index 0000000..70330ce
--- /dev/null
+++ b/res/drawable/frame_gallery_preview_album_sublevel_selected.png
Binary files differ
diff --git a/res/drawable/frame_gallery_thumb_uploaded_overlay.png b/res/drawable/frame_gallery_thumb_uploaded_overlay.png
new file mode 100755
index 0000000..85b1fc8
--- /dev/null
+++ b/res/drawable/frame_gallery_thumb_uploaded_overlay.png
Binary files differ
diff --git a/res/drawable/frame_overlay_gallery_camera.png b/res/drawable/frame_overlay_gallery_camera.png
new file mode 100755
index 0000000..a1fcfff
--- /dev/null
+++ b/res/drawable/frame_overlay_gallery_camera.png
Binary files differ
diff --git a/res/drawable/frame_overlay_gallery_downloaded.png b/res/drawable/frame_overlay_gallery_downloaded.png
new file mode 100755
index 0000000..70853ec
--- /dev/null
+++ b/res/drawable/frame_overlay_gallery_downloaded.png
Binary files differ
diff --git a/res/drawable/frame_overlay_gallery_video.png b/res/drawable/frame_overlay_gallery_video.png
new file mode 100755
index 0000000..5381146
--- /dev/null
+++ b/res/drawable/frame_overlay_gallery_video.png
Binary files differ
diff --git a/res/drawable/grid_background.xml b/res/drawable/grid_background.xml
new file mode 100644
index 0000000..8e0369e
--- /dev/null
+++ b/res/drawable/grid_background.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* grid_background.xml
+**
+** Copyright 2006, 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.
+*/
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:drawable="@drawable/image_border_bg_pressed_blue" />
+ <item android:state_focused="true" android:drawable="@drawable/image_border_bg_focus_blue" />
+ <item android:drawable="@drawable/image_border_bg_normal" />
+</selector>
diff --git a/res/drawable/highlight_unfocus.9.png b/res/drawable/highlight_unfocus.9.png
new file mode 100644
index 0000000..2a505b8
--- /dev/null
+++ b/res/drawable/highlight_unfocus.9.png
Binary files differ
diff --git a/res/drawable/ic_camera_flash.png b/res/drawable/ic_camera_flash.png
new file mode 100755
index 0000000..58265bf
--- /dev/null
+++ b/res/drawable/ic_camera_flash.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_auto_focus_green.png b/res/drawable/ic_camera_indicator_auto_focus_green.png
new file mode 100644
index 0000000..7bc09db
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_auto_focus_green.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_flash_auto.png b/res/drawable/ic_camera_indicator_flash_auto.png
new file mode 100755
index 0000000..a2809f1
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_flash_auto.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_flash_off.png b/res/drawable/ic_camera_indicator_flash_off.png
new file mode 100755
index 0000000..9a28b10
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_flash_off.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_flash_on.png b/res/drawable/ic_camera_indicator_flash_on.png
new file mode 100755
index 0000000..01fb9a4
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_flash_on.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_memory.9.png b/res/drawable/ic_camera_indicator_memory.9.png
new file mode 100644
index 0000000..ba4e1f5
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_memory.9.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_photo.png b/res/drawable/ic_camera_indicator_photo.png
new file mode 100755
index 0000000..ea981f4
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_photo.png
Binary files differ
diff --git a/res/drawable/ic_camera_indicator_video.png b/res/drawable/ic_camera_indicator_video.png
new file mode 100755
index 0000000..eaaa9e4
--- /dev/null
+++ b/res/drawable/ic_camera_indicator_video.png
Binary files differ
diff --git a/res/drawable/ic_camera_select_image.png b/res/drawable/ic_camera_select_image.png
new file mode 100755
index 0000000..848123c
--- /dev/null
+++ b/res/drawable/ic_camera_select_image.png
Binary files differ
diff --git a/res/drawable/ic_camera_select_video.png b/res/drawable/ic_camera_select_video.png
new file mode 100755
index 0000000..8d63517
--- /dev/null
+++ b/res/drawable/ic_camera_select_video.png
Binary files differ
diff --git a/res/drawable/ic_gallery_back_to_entry.png b/res/drawable/ic_gallery_back_to_entry.png
new file mode 100755
index 0000000..ad297ae
--- /dev/null
+++ b/res/drawable/ic_gallery_back_to_entry.png
Binary files differ
diff --git a/res/drawable/ic_gallery_empty.png b/res/drawable/ic_gallery_empty.png
new file mode 100755
index 0000000..b140f84
--- /dev/null
+++ b/res/drawable/ic_gallery_empty.png
Binary files differ
diff --git a/res/drawable/ic_gallery_finish.png b/res/drawable/ic_gallery_finish.png
new file mode 100755
index 0000000..550e86a
--- /dev/null
+++ b/res/drawable/ic_gallery_finish.png
Binary files differ
diff --git a/res/drawable/ic_gallery_picasa_upload.png b/res/drawable/ic_gallery_picasa_upload.png
new file mode 100755
index 0000000..211f93e
--- /dev/null
+++ b/res/drawable/ic_gallery_picasa_upload.png
Binary files differ
diff --git a/res/drawable/ic_gallery_upload_to.png b/res/drawable/ic_gallery_upload_to.png
new file mode 100755
index 0000000..b8a338f
--- /dev/null
+++ b/res/drawable/ic_gallery_upload_to.png
Binary files differ
diff --git a/res/drawable/ic_gallery_youtube_upload.png b/res/drawable/ic_gallery_youtube_upload.png
new file mode 100755
index 0000000..21c3820
--- /dev/null
+++ b/res/drawable/ic_gallery_youtube_upload.png
Binary files differ
diff --git a/res/drawable/ic_launcher_camera.png b/res/drawable/ic_launcher_camera.png
new file mode 100755
index 0000000..9bb4c61
--- /dev/null
+++ b/res/drawable/ic_launcher_camera.png
Binary files differ
diff --git a/res/drawable/ic_launcher_gallery.png b/res/drawable/ic_launcher_gallery.png
new file mode 100755
index 0000000..965fb71
--- /dev/null
+++ b/res/drawable/ic_launcher_gallery.png
Binary files differ
diff --git a/res/drawable/ic_menu_view_details.png b/res/drawable/ic_menu_view_details.png
new file mode 100644
index 0000000..a5a184f
--- /dev/null
+++ b/res/drawable/ic_menu_view_details.png
Binary files differ
diff --git a/res/drawable/ic_video_empty.png b/res/drawable/ic_video_empty.png
new file mode 100755
index 0000000..1209f86
--- /dev/null
+++ b/res/drawable/ic_video_empty.png
Binary files differ
diff --git a/res/drawable/image_border_bg_focus_blue.9.png b/res/drawable/image_border_bg_focus_blue.9.png
new file mode 100644
index 0000000..89debd2
--- /dev/null
+++ b/res/drawable/image_border_bg_focus_blue.9.png
Binary files differ
diff --git a/res/drawable/image_border_bg_focus_blue9.png b/res/drawable/image_border_bg_focus_blue9.png
new file mode 100644
index 0000000..89debd2
--- /dev/null
+++ b/res/drawable/image_border_bg_focus_blue9.png
Binary files differ
diff --git a/res/drawable/image_border_bg_normal.9.png b/res/drawable/image_border_bg_normal.9.png
new file mode 100644
index 0000000..18e3607
--- /dev/null
+++ b/res/drawable/image_border_bg_normal.9.png
Binary files differ
diff --git a/res/drawable/image_border_bg_normal9.png b/res/drawable/image_border_bg_normal9.png
new file mode 100644
index 0000000..18e3607
--- /dev/null
+++ b/res/drawable/image_border_bg_normal9.png
Binary files differ
diff --git a/res/drawable/image_border_bg_pressed_blue.9.png b/res/drawable/image_border_bg_pressed_blue.9.png
new file mode 100644
index 0000000..94fa74f
--- /dev/null
+++ b/res/drawable/image_border_bg_pressed_blue.9.png
Binary files differ
diff --git a/res/drawable/image_border_bg_pressed_blue9.png b/res/drawable/image_border_bg_pressed_blue9.png
new file mode 100644
index 0000000..94fa74f
--- /dev/null
+++ b/res/drawable/image_border_bg_pressed_blue9.png
Binary files differ
diff --git a/res/drawable/indicator_autocrop.png b/res/drawable/indicator_autocrop.png
new file mode 100644
index 0000000..d960b1f
--- /dev/null
+++ b/res/drawable/indicator_autocrop.png
Binary files differ
diff --git a/res/drawable/play_video.png b/res/drawable/play_video.png
new file mode 100644
index 0000000..0c10731
--- /dev/null
+++ b/res/drawable/play_video.png
Binary files differ
diff --git a/res/drawable/popup_camera_toast.9.png b/res/drawable/popup_camera_toast.9.png
new file mode 100755
index 0000000..c1f0c1f
--- /dev/null
+++ b/res/drawable/popup_camera_toast.9.png
Binary files differ
diff --git a/res/layout/camera.xml b/res/layout/camera.xml
new file mode 100644
index 0000000..1bd5a14
--- /dev/null
+++ b/res/layout/camera.xml
@@ -0,0 +1,118 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal">
+
+ <SurfaceView
+ android:id="@+id/camera_preview"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_centerInParent="true"
+ />
+
+ <ImageView
+ android:id="@+id/blackout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:visibility="invisible"
+ />
+
+ <ImageView
+ android:id="@+id/focus_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:src="@drawable/ic_camera_indicator_auto_focus_green"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="6dip"
+ android:layout_marginTop="6dip"
+ android:scaleType="center"/>
+
+ <TextView
+ android:id="@+id/hint_toast"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:background="@drawable/popup_camera_toast"
+ android:layout_width="200dip"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:text="@string/camera_button_hint"
+ android:textSize="18sp"
+ android:visibility="visible"/>
+
+ <AbsoluteLayout
+ android:background="#CC666666"
+ android:visibility="invisible"
+ android:id="@+id/post_picture_panel"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="false">
+ <View android:layout_width="fill_parent"
+ android:layout_height="1dip"
+ android:background="@android:drawable/divider_horizontal_dark" />
+ <LinearLayout
+ android:paddingLeft="7dip"
+ android:paddingRight="7dip"
+ android:paddingTop="6dip"
+ android:paddingBottom="6dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:background="#00000000"
+ android:orientation="horizontal">
+ <Button
+ android:id="@+id/save"
+ android:textSize="18sp"
+ android:drawableTop="@android:drawable/ic_menu_save"
+ android:layout_width="112dip"
+ android:layout_height="wrap_content"
+ android:text="@string/camera_keep"
+ />
+ <View android:layout_width="6dip" android:layout_height="6dip" />
+ <Button
+ android:id="@+id/setas"
+ android:textSize="18sp"
+ android:drawableTop="@android:drawable/ic_menu_set_as"
+ android:layout_width="112dip"
+ android:layout_height="wrap_content"
+ android:text="@string/camera_set"
+ />
+ <View android:layout_width="6dip" android:layout_height="6dip" />
+ <Button
+ android:id="@+id/share"
+ android:textSize="18sp"
+ android:drawableTop="@android:drawable/ic_menu_share"
+ android:layout_width="112dip"
+ android:layout_height="wrap_content"
+ android:text="@string/camera_share"
+ />
+ <View android:layout_width="6dip" android:layout_height="6dip" />
+ <Button
+ android:id="@+id/discard"
+ android:textSize="18sp"
+ android:drawableTop="@android:drawable/ic_menu_delete"
+ android:layout_width="112dip"
+ android:layout_height="wrap_content"
+ android:text="@string/camera_toss"
+ />
+ </LinearLayout>
+ </AbsoluteLayout>
+</RelativeLayout>
+
diff --git a/res/layout/cropimage.xml b/res/layout/cropimage.xml
new file mode 100644
index 0000000..4f3dcb7
--- /dev/null
+++ b/res/layout/cropimage.xml
@@ -0,0 +1,58 @@
+<?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="fill_parent"
+ android:layout_height="fill_parent">
+
+ <RelativeLayout
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal">
+
+ <view class="com.android.camera.CropImage$CropImageView" android:id="@+id/image"
+ android:background="#55000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_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>
+
diff --git a/res/layout/custom_gallery_title.xml b/res/layout/custom_gallery_title.xml
new file mode 100644
index 0000000..0652678
--- /dev/null
+++ b/res/layout/custom_gallery_title.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/screen" android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical">
+
+ <TextView android:id="@+id/left_text"
+ android:gravity="center_vertical"
+ style="?android:attr/windowTitleStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent" android:paddingLeft="5dip"
+ android:layout_alignParentLeft="true" />
+
+ <TextView android:id="@+id/right_text"
+ android:gravity="center_vertical"
+ style="?android:attr/windowTitleStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent" android:paddingRight="5dip"
+ android:layout_alignParentRight="true"
+ android:visibility="gone" />
+
+ <LinearLayout android:id="@+id/loading_indicator"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:paddingTop="2dip"
+ android:layout_alignParentRight="true"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content">
+
+ <TextView android:id="@+id/loading_text"
+ android:gravity="center_vertical"
+ style="?android:attr/windowTitleStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingRight="5dip"
+ android:layout_alignParentRight="true" />
+
+ <ProgressBar android:id="@android:id/progress"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:gravity="center_vertical"
+ android:paddingRight="5dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ </LinearLayout>
+</RelativeLayout>
diff --git a/res/layout/details.xml b/res/layout/details.xml
new file mode 100644
index 0000000..482e567
--- /dev/null
+++ b/res/layout/details.xml
@@ -0,0 +1,159 @@
+<?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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:padding="10dip"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/details_title_label"
+ android:textSize="10sp"
+ />
+
+ </LinearLayout>
+
+ <EditText
+ android:id="@+id/title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/details_title_text"
+ android:autoText="true"
+ android:capitalize="words"
+ android:textSize="10sp"
+ android:textColor="#FF661700"
+ />
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/details_description_label"
+ android:textSize="10sp"
+ />
+
+ <EditText
+ android:id="@+id/description"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/details_description_text"
+ android:autoText="true"
+ android:capitalize="sentences"
+ android:textSize="10sp"
+ android:textColor="#FF661700"
+ />
+
+ <TextView android:id="@+id/tags_label"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/details_tags_label"
+ android:textSize="10sp"
+ android:visibility="gone"
+ />
+
+ <EditText
+ android:id="@+id/tags"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/details_tags_text"
+ android:textSize="10sp"
+ android:textColor="#FF661700"
+ android:autoText="false"
+ android:capitalize="none"
+ android:visibility="gone"
+ />
+
+ <LinearLayout android:id="@+id/categories"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/details_category_label"
+ android:textSize="10sp"
+ />
+
+ <Spinner android:id="@+id/category"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawSelectorOnTop="false"
+ android:textSize="10sp"
+ />
+ </LinearLayout>
+
+ <LinearLayout android:id="@+id/languages"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/details_language_label"
+ android:textSize="10sp"
+ />
+
+ <Spinner android:id="@+id/language"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:drawSelectorOnTop="false"
+ android:textSize="10sp"
+ />
+ </LinearLayout>
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <RadioGroup android:id="@+id/publicprivate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <RadioButton android:id="@+id/publicView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/details_publicView_text"
+ />
+
+ <RadioButton android:id="@+id/privateView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="false"
+ android:text="@string/details_privateView_text"
+ />
+ </RadioGroup>
+
+ <Button android:id="@+id/save"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/details_save_text"
+ android:layout_alignParentRight="true"
+ />
+ </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/detailsview.xml b/res/layout/detailsview.xml
new file mode 100644
index 0000000..21cac08
--- /dev/null
+++ b/res/layout/detailsview.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scroll_view"
+ android:orientation="vertical"
+ android:layout_width="300dip"
+ android:layout_height="fill_parent">
+
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="10dip"
+ android:paddingRight="10dip"
+ android:paddingBottom="10dp">
+
+ <LinearLayout
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView
+ android:id="@+id/details_thumbnail_image"
+ android:layout_width="64dip"
+ android:layout_height="64dip"/>
+
+ <TextView
+ android:id="@+id/details_image_title"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:paddingLeft="6dip"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+ <TableLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dip">
+
+ <TableRow>
+ <TextView
+ android:id="@+id/details_attrname_1"
+ android:gravity="right"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+ <TextView
+ android:id="@+id/details_attrvalu_1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingLeft="5dip"
+ android:textColor="?android:attr/textColorPrimary"/>
+ </TableRow>
+
+ <TableRow>
+ <TextView
+ android:id="@+id/details_attrname_2"
+ android:gravity="right"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ <TextView
+ android:id="@+id/details_attrvalu_2"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingLeft="5dip"
+ android:textColor="?android:attr/textColorPrimary"/>
+ </TableRow>
+
+ <TableRow
+ android:id="@+id/details_daterow">
+ <TextView
+ android:id="@+id/details_attrname_3"
+ android:gravity="right"
+ android:textAppearance="?android:attr/textAppearanceSmall"/>
+ <TextView
+ android:id="@+id/details_attrvalu_3"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:paddingLeft="5dip"
+ android:textColor="?android:attr/textColorPrimary"/>
+ </TableRow>
+ </TableLayout>
+
+ </LinearLayout>
+
+</ScrollView>
diff --git a/res/layout/gallery.xml b/res/layout/gallery.xml
new file mode 100644
index 0000000..61a0db2
--- /dev/null
+++ b/res/layout/gallery.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal">
+
+ <View android:background="#FF000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ />
+
+ <ImageView android:id="@+id/switcher1"
+ android:background="#FF000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:visibility="invisible"
+ />
+
+ <ImageView android:id="@+id/switcher2"
+ android:background="#55000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ android:visibility="invisible"
+ />
+
+ <Gallery android:id="@+id/gallery"
+ android:background="#55000000"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+
+ android:gravity="center_vertical"
+ android:spacing="16dp"
+ />
+
+</RelativeLayout>
diff --git a/res/layout/gallery_picker_item.xml b/res/layout/gallery_picker_item.xml
new file mode 100644
index 0000000..5536dad
--- /dev/null
+++ b/res/layout/gallery_picker_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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="160dp" android:paddingTop="0dip"
+ android:paddingBottom="0dip" android:paddingLeft="0dip"
+ android:layout_height="wrap_content">
+
+ <LinearLayout android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal">
+
+ <com.android.camera.GalleryPickerItem android:id="@+id/thumbnail"
+ android:layout_width="142dip"
+ android:layout_height="142dip"
+ android:scaleType="centerCrop" />
+
+ <TextView android:id="@+id/title"
+ android:paddingTop="3dip"
+ android:paddingBottom="9dip"
+ android:layout_width="wrap_content"
+ android:textSize="14sp"
+ android:maxLines="2"
+ android:textColor="#ffffffff"
+ android:ellipsize="end"
+ android:layout_height="wrap_content" />
+
+ </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/gallerypicker.xml b/res/layout/gallerypicker.xml
new file mode 100644
index 0000000..42dbc7e
--- /dev/null
+++ b/res/layout/gallerypicker.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+
+<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#000000">
+
+ <GridView android:id="@+id/albums"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingTop="2dip"
+ android:numColumns="auto_fit"
+ android:columnWidth="160dp"
+ android:stretchMode="spacingWidth"
+ android:drawSelectorOnTop="false" />
+
+</AbsoluteLayout>
+
diff --git a/res/layout/gallerysettings.xml b/res/layout/gallerysettings.xml
new file mode 100644
index 0000000..b513813
--- /dev/null
+++ b/res/layout/gallerysettings.xml
@@ -0,0 +1,63 @@
+<?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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/scroll_view"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#1C1C1C">
+
+ <LinearLayout android:id="@+id/layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:paddingTop="30dip"
+ android:orientation="vertical">
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="50dip">
+ <TextView
+ android:layout_height="wrap_content"
+ android:textColor="#FFFFFFFF"
+ android:layout_width="0dip"
+ android:textSize="10sp"
+ android:layout_weight="50"
+ android:gravity="right"
+ android:singleLine="false"
+ android:text="@string/gallerysettings_duration" />
+
+ <Spinner android:id="@+id/slide_show_speed"
+ android:layout_width="0dip"
+ android:layout_weight="50"
+ android:layout_marginRight="10dip"
+ android:layout_marginLeft="20dip"
+ android:layout_height="wrap_content">
+ </Spinner>
+
+ </LinearLayout>
+
+ <Button android:id="@+id/done"
+ android:layout_width="80dip"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="70dip"
+ android:layout_marginRight="10dip"
+ android:gravity="bottom"
+ android:layout_gravity="right"
+ android:text="@string/camerasettings_done" />
+
+ </LinearLayout>
+</ScrollView>
+
diff --git a/res/layout/grid.xml b/res/layout/grid.xml
new file mode 100644
index 0000000..2bdbc8a
--- /dev/null
+++ b/res/layout/grid.xml
@@ -0,0 +1,44 @@
+<!--
+/**
+ * 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.
+ */
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#FFFFFF">
+
+ <ImageSwitcher android:id="@+id/switcher"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentLeft="true"
+ />
+
+ <GridView android:id="@+id/gallery"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+
+ android:padding="0dp"
+ android:verticalSpacing="0dp"
+ android:horizontalSpacing="0dp"
+ android:stretchMode="columnWidth"
+
+ android:gravity="center"
+ />
+
+</RelativeLayout>
+
diff --git a/res/layout/image_gallery.xml b/res/layout/image_gallery.xml
new file mode 100644
index 0000000..d4b9db6
--- /dev/null
+++ b/res/layout/image_gallery.xml
@@ -0,0 +1,44 @@
+<?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.
+-->
+
+<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <GridView
+ android:id="@+id/grid"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="12dp"
+ android:verticalSpacing="8dp"
+ android:horizontalSpacing="8dp"
+ android:stretchMode="spacingWidth"
+ android:gravity="center"
+ android:drawSelectorOnTop="false"
+ android:listSelector="@drawable/grid_background"
+ android:numColumns="auto_fit"
+ android:columnWidth="64dp"
+ android:fadingEdge="none"
+ />
+
+ <TextView android:id="@+id/NoImageView"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:text="@string/image_gallery_NoImageView_text"
+ android:textColor="#FFFFFFFF"
+ />
+</AbsoluteLayout>
+
diff --git a/res/layout/image_gallery_2.xml b/res/layout/image_gallery_2.xml
new file mode 100644
index 0000000..0f4926c
--- /dev/null
+++ b/res/layout/image_gallery_2.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <view class="com.android.camera.ImageGallery2$GridViewSpecial"
+ android:id="@+id/grid"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:listSelector="@drawable/grid_background"
+ />
+
+ <RelativeLayout android:id="@+id/no_images"
+ android:visibility="gone"
+ android:orientation="vertical"
+ android:layout_centerInParent="true"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <ImageView
+ android:id="@+id/no_pictures_image"
+ android:layout_centerInParent="true"
+ android:layout_gravity="center"
+ android:background="@drawable/ic_gallery_empty"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ />
+
+ <TextView
+ android:layout_below="@id/no_pictures_image"
+ android:layout_centerHorizontal="true"
+ android:paddingTop="5dip"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/image_gallery_NoImageView_text"
+ android:textColor="#FFBEBEBE"
+ android:textSize="18dip"
+ />
+ </RelativeLayout>
+
+</RelativeLayout>
+
diff --git a/res/layout/slide_show.xml b/res/layout/slide_show.xml
new file mode 100644
index 0000000..e9bfe0f
--- /dev/null
+++ b/res/layout/slide_show.xml
@@ -0,0 +1,27 @@
+<?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="fill_parent"
+ android:layout_height="fill_parent">
+
+ <view android:id="@+id/imageview"
+ class="com.android.camera.SlideShow$ImageViewTouch"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ />
+
+</FrameLayout>
diff --git a/res/layout/viewimage.xml b/res/layout/viewimage.xml
new file mode 100644
index 0000000..de042f1
--- /dev/null
+++ b/res/layout/viewimage.xml
@@ -0,0 +1,96 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/rootLayout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <AbsoluteLayout android:id="@+id/slideShowContainer"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <view android:id="@+id/image1_slideShow"
+ class="com.android.camera.ViewImage$ImageViewTouch"
+ android:background="#00000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ />
+ <view android:id="@+id/image2_slideShow"
+ class="com.android.camera.ViewImage$ImageViewTouch"
+ android:background="#00000000"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ />
+ </AbsoluteLayout>
+
+ <AbsoluteLayout android:id="@+id/abs"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+ <view class="com.android.camera.ViewImage$ScrollHandler" android:id="@+id/scroller"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="horizontal"
+
+ android:background="#FF000000"
+ android:layout_x="0dip"
+ android:layout_y="0dip">
+
+ <view android:id="@+id/image1"
+ class="com.android.camera.ViewImage$ImageViewTouch"
+ android:background="#FF000000"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent" />
+
+ <View android:id="@+id/padding1"
+ android:layout_height="fill_parent"
+ android:layout_width="0dip" />
+
+ <view android:id="@+id/image2"
+ class="com.android.camera.ViewImage$ImageViewTouch"
+ android:background="#FF000000"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"/>
+
+ <View android:id="@+id/padding2"
+ android:layout_height="fill_parent"
+ android:layout_width="0dip" />
+
+ <view android:id="@+id/image3"
+ class="com.android.camera.ViewImage$ImageViewTouch"
+ android:background="#FF000000"
+ android:layout_width="0dip"
+ android:layout_height="fill_parent"/>
+ </view>
+ </AbsoluteLayout>
+
+ <ImageView android:id="@+id/prev_image"
+ android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:visibility="invisible"
+ android:src="@drawable/btn_camera_arrow_left"
+ />
+
+ <ImageView android:id="@+id/next_image"
+ android:layout_width="wrap_content" android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:visibility="invisible"
+ android:src="@drawable/btn_camera_arrow_right"
+ />
+
+</RelativeLayout>
+
diff --git a/res/layout/viewvideo.xml b/res/layout/viewvideo.xml
new file mode 100644
index 0000000..49af24b
--- /dev/null
+++ b/res/layout/viewvideo.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:background="#FF000000"
+ >
+
+ <VideoView android:id="@+id/video"
+ android:layout_centerInParent="true"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+</RelativeLayout>
+
diff --git a/res/layout/youtube_upload_info.xml b/res/layout/youtube_upload_info.xml
new file mode 100644
index 0000000..fa62a77
--- /dev/null
+++ b/res/layout/youtube_upload_info.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+>
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content">
+
+
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/upload_info_title"
+ android:textSize="12sp" />
+
+ <EditText android:id="@+id/video_title"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" android:autoText="true"
+ android:capitalize="words" android:textSize="12sp" />
+
+ <TextView android:layout_width="wrap_content"
+ android:layout_height="wrap_content" android:paddingTop="10dip"
+ android:text="@string/details_category_label"
+ android:textSize="12sp" />
+
+ <Spinner android:id="@+id/category"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:drawSelectorOnTop="false" android:textSize="12sp" />
+
+ <TextView android:id="@+id/tags_label"
+ android:layout_width="fill_parent" android:paddingTop="10dip"
+ android:layout_height="wrap_content"
+ android:text="@string/upload_info_tags" android:textSize="12sp" />
+
+ <EditText android:id="@+id/video_tags"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" android:textSize="12sp"
+ android:autoText="false" android:capitalize="none" />
+
+ <TextView android:layout_width="fill_parent"
+ android:paddingTop="10dip" android:layout_height="wrap_content"
+ android:text="@string/upload_info_description"
+ android:textSize="12sp" />
+
+ <EditText android:id="@+id/video_description"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" android:autoText="true"
+ android:capitalize="sentences" android:textSize="12sp" />
+
+ <LinearLayout android:orientation="horizontal"
+ android:layout_width="fill_parent" android:paddingTop="10dip"
+ android:layout_height="wrap_content">
+
+ <CheckBox android:id="@+id/public_or_private"
+ android:layout_width="0dip" android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/upload_info_private" />
+
+ <Button android:id="@+id/do_upload"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/upload_info_upload" />
+ </LinearLayout>
+</LinearLayout>
+</ScrollView>
diff --git a/res/raw/camera_click.ogg b/res/raw/camera_click.ogg
new file mode 100644
index 0000000..be0a0d3
--- /dev/null
+++ b/res/raw/camera_click.ogg
Binary files differ
diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml
new file mode 100644
index 0000000..de897d6
--- /dev/null
+++ b/res/values-cs/strings.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="autocrop_before_query">Automaticky nejprve oříznout?</string>
+ <string name="autocrop_no">Ano</string>
+ <string name="autocrop_yes">Ano</string>
+ <string name="best_res">Vysoká</string>
+ <string name="camera_NoStorageView_text">Před použitím fotoaparátu vložte kartu SD</string>
+ <string name="camera_ZoomIn_text">Přiblížit</string>
+ <string name="camera_ZoomOut_text">Oddálit</string>
+ <string name="camera_button_hint">Stisknutím tlačítka Sejmout pořídíte snímek</string>
+ <string name="camera_crop">Automaticky oříznout</string>
+ <string name="camera_done">Nový obrázek</string>
+ <string name="camera_flash_auto">Automatický blesk</string>
+ <string name="camera_flash_off">Blesk VYPNUTÝ</string>
+ <string name="camera_flash_on">Blesk ZAPNUTÝ</string>
+ <string name="camera_flash_setting">Nastavení blesku</string>
+ <string name="camera_gallery_photos_text">Obrázky</string>
+ <string name="camera_gallery_videos_text">Videa</string>
+ <string name="camera_keeping">Uchovávání\u2026</string>
+ <string name="camera_label">Fotoaparát</string>
+ <string name="camera_mode_text">Režim</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_playvideo">Přehrát</string>
+ <string name="camera_record_text">Zaznamenat</string>
+ <string name="camera_selectphoto">Vybrat tento obrázek</string>
+ <string name="camera_selectvideo">Vybrat video</string>
+ <string name="camera_set">Nastavit jako\u2026</string>
+ <string name="camera_setas_contact">Obrázek kontaktu</string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Tapeta</string>
+ <string name="camera_share">Sdílet</string>
+ <string name="camera_shareby_email">E-mail</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (všechny obrázky)</string>
+ <string name="camera_switch_to_photo">Přepnout na obrázek</string>
+ <string name="camera_switch_to_video">Přepnout na video</string>
+ <string name="camera_takenewphoto">Pořídit nový snímek</string>
+ <string name="camera_takenewvideo">Pořídit nové video</string>
+ <string name="camera_toss">Odstranit</string>
+ <string name="camera_tossing">Odstraňování\u2026</string>
+ <string name="camera_zoom_1_text">Přiblížit 1,3x</string>
+ <string name="camera_zoom_2_text">Přiblížit 2x</string>
+ <string name="camera_zoom_3_text">Přiblížit 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">Nastavení</string>
+ <string name="camerasettings_autoupload_label">Automaticky odeslat obrázky na web</string>
+ <string name="camerasettings_done">Hotovo</string>
+ <string name="camerasettings_duration_label">Doba trvání:</string>
+ <string name="camerasettings_image_quality_label">Kvalita obrázku:</string>
+ <string name="camerasettings_no">Ne</string>
+ <string name="camerasettings_off">Vypnuto</string>
+ <string name="camerasettings_on">Zapnuto</string>
+ <string name="camerasettings_play_click_sound_label">Zvuk závěrky</string>
+ <string name="camerasettings_preferred_view_label">Upřednostňované zobrazení:</string>
+ <string name="camerasettings_resolution_label">Rozlišení:</string>
+ <string name="camerasettings_test2_label">test2:</string>
+ <string name="camerasettings_test3_label">test3:</string>
+ <string name="camerasettings_test4_label">test4:</string>
+ <string name="camerasettings_use_sd_card_label">Uložit obrázky na kartu SD</string>
+ <string name="camerasettings_yes">Ano</string>
+ <string name="cancel">Storno</string>
+ <string name="context_menu_header">Možnosti obrázků</string>
+ <string name="crop_discard_text">Zrušit</string>
+ <string name="crop_help">Přidržením ALT změňte velikost</string>
+ <string name="crop_label">Oříznout obrázek</string>
+ <string name="crop_save_text">Uložit</string>
+ <string name="delete">Odstranit</string>
+ <string name="details">Podrobnosti</string>
+ <string name="details_category_label">Kategorie:</string>
+ <string name="details_date_taken">Datum pořízení:</string>
+ <string name="details_description_label">Popis:</string>
+ <string name="details_description_text">testpopis</string>
+ <string name="details_file_size">Velikost souboru:</string>
+ <string name="details_image_resolution">Rozlišení:</string>
+ <string name="details_language_label">Jazyk:</string>
+ <string name="details_panel_title">Podrobnosti</string>
+ <string name="details_privateView_text">Soukromé</string>
+ <string name="details_publicView_text">Veřejné</string>
+ <string name="details_save_text">Uložit</string>
+ <string name="details_tags_label">Značky:</string>
+ <string name="details_tags_text">monstrum</string>
+ <string name="details_title_label">Název:</string>
+ <string name="details_title_text">myvideo</string>
+ <string name="details_uploaded_text">Úspěšně odesláno</string>
+ <string name="edit">Upravit</string>
+ <string name="error_label">Chyba</string>
+ <string name="gallery_label">Fotografie a videoklipy</string>
+ <string name="gallery_large">Velký</string>
+ <string name="gallery_picker_label">Alba</string>
+ <string name="gallery_small">Malá</string>
+ <string name="gallerysettings_duration">Doba zobrazení každého snímku v prezentaci</string>
+ <string name="gallerysettings_speed1">1 sekunda</string>
+ <string name="gallerysettings_speed2">3 sekundy</string>
+ <string name="gallerysettings_speed3">5 sekund</string>
+ <string name="high">Vysoká</string>
+ <string name="image_gallery_NoImageView_text">Žádné obrázky</string>
+ <string name="image_gallery_picker_images">Obrázky</string>
+ <string name="image_gallery_picker_videos">Videa</string>
+ <string name="low">Nízká</string>
+ <string name="low_res">Nízká</string>
+ <string name="med">Střední</string>
+ <string name="medium_res">Střední</string>
+ <string name="multiface_crop_help">Vyberte obličej, kterým chcete začít</string>
+ <string name="no">Ne</string>
+ <string name="no_images">Album je prázdné</string>
+ <string name="no_storage">Před použitím fotoaparátu vložte kartu SD</string>
+ <string name="no_way_to_share_image">Obrázek nelze uložit</string>
+ <string name="no_way_to_share_video">Toto video nelze sdílet</string>
+ <string name="not_enough_space">Paměťová karta je plná</string>
+ <string name="ok">OK</string>
+ <string name="photos_gallery_title">Obrázky</string>
+ <string name="picasa_upload_label">Odeslání na web Picasa</string>
+ <string name="pick_photos_gallery_title">Vybrat obrázek</string>
+ <string name="pick_videos_gallery_title">Vybrat video</string>
+ <string name="pref_camera_autoupload_summary">Výběrem nastavíte automatické odesílání obrázků na web Picasa</string>
+ <string name="pref_camera_autoupload_title">Automaticky odesílat obrázky</string>
+ <string name="pref_camera_category">Fotoaparát</string>
+ <string name="pref_camera_recordlocation_summary">Výběrem nastavíte automatický záznam polohy do obrázků</string>
+ <string name="pref_camera_recordlocation_title">Zaznamenávat polohu do obrázků</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Název alba Picasa</string>
+ <string name="pref_camera_upload_albumname_summary">Zadejte název cílového alba pro obrázky</string>
+ <string name="pref_camera_upload_albumname_title">Název alba Picasa</string>
+ <string name="pref_gallery_category">Zobrazení alba obrázků</string>
+ <string name="pref_gallery_size_dialogtitle">Velikost obrázku</string>
+ <string name="pref_gallery_size_summary">Vyberte zobrazovanou velikost obrázků v albu</string>
+ <string name="pref_gallery_size_title">Velikost obrázku</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Interval prezentace</string>
+ <string name="pref_gallery_slideshow_interval_summary">Vyberte dobu zobrazení každého snímku v prezentaci</string>
+ <string name="pref_gallery_slideshow_interval_title">Interval prezentace</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Opakovat prezentaci?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Výběrem přehrajete prezentaci vícekrát</string>
+ <string name="pref_gallery_slideshow_repeat_title">Opakovat prezentaci</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Náhodné zobrazování obrázků?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Výběrem nastavíte zobrazování obrázků v náhodném pořadí</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Náhodné zobrazování snímků</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Přechod prezentace</string>
+ <string name="pref_gallery_slideshow_transition_summary">Vyberte efekt, který bude použit při přechodu mezi snímky</string>
+ <string name="pref_gallery_slideshow_transition_title">Přechod prezentace</string>
+ <string name="pref_gallery_sort_dialogtitle">Řazení obrázků</string>
+ <string name="pref_gallery_sort_summary">Vyberte pořadí řazení obrázků v albu</string>
+ <string name="pref_gallery_sort_title">Řazení obrázků</string>
+ <string name="pref_slideshow_category">Prezentace</string>
+ <string name="preferences_label">Nastavení fotoaparátu</string>
+ <string name="preview">Náhled</string>
+ <string name="record">Zaznamenat</string>
+ <string name="rotate">Otočit</string>
+ <string name="rotate_left">Otočit vlevo</string>
+ <string name="rotate_right">Otočit vpravo</string>
+ <string name="runningFaceDetection">Počkejte prosím\u2026</string>
+ <string name="save">Uložit</string>
+ <string name="savingImage">Ukládání obrázku</string>
+ <string name="sec1">1 s</string>
+ <string name="sec2">2 s</string>
+ <string name="sec3">3 s</string>
+ <string name="sec4">4 s</string>
+ <string name="sec5">5 s</string>
+ <string name="sendImage">Sdílet tento obrázek prostřednictvím</string>
+ <string name="sendVideo">Sdílet toto video prostřednictvím</string>
+ <string name="set_wallpaper">Nastavit jako tapetu</string>
+ <string name="settings">Nastavení</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Prezentace</string>
+ <string name="stop">Zastavit</string>
+ <string name="testexif">Test Exif</string>
+ <string name="upload">Odeslat</string>
+ <string name="upload_all">Odeslat vše</string>
+ <string name="upload_default_category_text">Zábava</string>
+ <string name="upload_default_description_text">místo pro popis</string>
+ <string name="upload_default_tags_text">video</string>
+ <string name="upload_dialog_title">Odeslat na web YouTube - zadejte podrobnosti o videoklipu</string>
+ <string name="upload_info_category">Kategorie:</string>
+ <string name="upload_info_description">Popis:</string>
+ <string name="upload_info_private">Není-li zaškrtnuto, uvidíte film jen vy</string>
+ <string name="upload_info_tags">Značky:</string>
+ <string name="upload_info_title">Název:</string>
+ <string name="upload_info_upload">Odeslat</string>
+ <string name="uploadingNPhotos">Odesílání <xliff:g id="counter">%d</xliff:g> obrázků</string>
+ <string name="uploadingNVideos">Odesílání <xliff:g id="counter">%d</xliff:g> videoklipů</string>
+ <string name="uploading_photos">Odesílání obrázků</string>
+ <string name="uploading_photos_2">Odesílání obrázků</string>
+ <string name="uploading_videos">Odesílání videoklipů</string>
+ <string name="uploading_videos_2">Odesílání videoklipů</string>
+ <string name="video_gallery_NoImageView_text">Žádná videa</string>
+ <string name="video_play">Přehrát</string>
+ <string name="videos_gallery_title">Videa</string>
+ <string name="view_label">Zobrazit obrázek</string>
+ <string name="view_video_label">Zobrazit video</string>
+ <string name="viewimage_comments_label_text">Komentáře</string>
+ <string name="viewimage_datetaken_label_text">Datum</string>
+ <string name="viewimage_digitalzoom_label_text">Digitální zoom</string>
+ <string name="viewimage_flash_label_text">Použitý blesk</string>
+ <string name="viewimage_gps_latitude_label_text">Zeměpisná šířka GPS</string>
+ <string name="viewimage_gps_longitude_label_text">Zeměpisná délka GPS</string>
+ <string name="viewimage_make_label_text">Vytvořit</string>
+ <string name="viewimage_model_label_text">Od</string>
+ <string name="viewimage_orientation_label_text">Orientace</string>
+ <string name="viewimage_res_label_text">Rozlišení</string>
+ <string name="viewimage_status_text">přiblížení tlačítkem Enter nebo mezerníkem</string>
+ <string name="wait">Počkejte prosím\u2026</string>
+ <string name="wallpaper">Nastavení tapety, počkejte prosím\u2026</string>
+ <string name="yes">Ano</string>
+ <string name="youtube_upload_label">Odeslání na web YouTube</string>
+ <string name="zoom">Lupa</string>
+</resources>
diff --git a/res/values-de-rDE/strings.xml b/res/values-de-rDE/strings.xml
new file mode 100644
index 0000000..2846dea
--- /dev/null
+++ b/res/values-de-rDE/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="all_images">Alle Bilder</string>
+ <string name="autocrop_before_query">Vorher zuschneiden?</string>
+ <string name="autocrop_no">Ja</string>
+ <string name="autocrop_yes">Ja</string>
+ <string name="best_res">Hoch</string>
+ <string name="camera_NoStorageView_text">Setzen Sie eine SD-Karte ein, bevor Sie die Kamera verwenden.</string>
+ <string name="camera_ZoomIn_text">Tele-Aufnahme</string>
+ <string name="camera_ZoomOut_text">Weitwinkel-Aufnahme</string>
+ <string name="camera_button_hint">Drücken Sie auf die Aufnahmetaste, um ein Bild aufzunehmen.</string>
+ <string name="camera_crop">Zuschneiden</string>
+ <string name="camera_done">Neues Bild</string>
+ <string name="camera_flash_auto">Autom. Blitz</string>
+ <string name="camera_flash_off">Blitz AUS</string>
+ <string name="camera_flash_on">Blitz EIN</string>
+ <string name="camera_flash_setting">Blitzeinstellung</string>
+ <string name="camera_gallery_photos_text">Bilder</string>
+ <string name="camera_gallery_videos_text">Videoalbum</string>
+ <string name="camera_keep">Speichern</string>
+ <string name="camera_keeping">Beibehalten\u2026</string>
+ <string name="camera_label">Kamera</string>
+ <string name="camera_mode_text">Modus</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_pick_wallpaper">Bilder</string>
+ <string name="camera_playvideo">Abspielen</string>
+ <string name="camera_record_text">Aufnehmen</string>
+ <string name="camera_selectphoto">Dieses Bild auswählen</string>
+ <string name="camera_selectvideo">Dieses Video auswählen</string>
+ <string name="camera_set">Einstellen als</string>
+ <string name="camera_setas_contact">Kontaktebild </string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Bildschirmhintergrund</string>
+ <string name="camera_setas_wallpaper_drm">Gekaufte Bilder</string>
+ <string name="camera_share">Freigeben</string>
+ <string name="camera_shareby_email">E-Mail</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (alle Bilder)</string>
+ <string name="camera_switch_to_photo">Zu Bild wechseln</string>
+ <string name="camera_switch_to_video">Zu Video wechseln</string>
+ <string name="camera_takenewphoto">Neues Bild aufnehmen</string>
+ <string name="camera_takenewvideo">Neues Video aufnehmen</string>
+ <string name="camera_toss">Löschen</string>
+ <string name="camera_tossing">Löschen\u2026</string>
+ <string name="camera_zoom_1_text">Zoom 1,3x</string>
+ <string name="camera_zoom_2_text">Zoom 2x</string>
+ <string name="camera_zoom_3_text">Zoom 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">Einstellungen</string>
+ <string name="camerasettings_autoupload_label">Bilder automatisch zum Web hochladen</string>
+ <string name="camerasettings_done">Fertig</string>
+ <string name="camerasettings_duration_label">Dauer:</string>
+ <string name="camerasettings_image_quality_label">Bildqualität:</string>
+ <string name="camerasettings_no">Nein</string>
+ <string name="camerasettings_off">Aus</string>
+ <string name="camerasettings_on">Ein</string>
+ <string name="camerasettings_play_click_sound_label">Auslöserton</string>
+ <string name="camerasettings_preferred_view_label">Bevorzugte Ansicht:</string>
+ <string name="camerasettings_resolution_label">Auflösung:</string>
+ <string name="camerasettings_test2_label">Test2:</string>
+ <string name="camerasettings_test3_label">Test3:</string>
+ <string name="camerasettings_test4_label">Test4:</string>
+ <string name="camerasettings_use_sd_card_label">Bilder auf SD-Karte speichern</string>
+ <string name="camerasettings_yes">Ja</string>
+ <string name="cancel">Abbrechen</string>
+ <string name="confirm_delete">Löschen bestätigen</string>
+ <string name="confirm_delete_message">Das Bild wird gelöscht.</string>
+ <string name="confirm_delete_title">Löschen</string>
+ <string name="context_menu_header">Bildoptionen</string>
+ <string name="crop_discard_text">Verwerfen</string>
+ <string name="crop_help">Für Größenänderung ALT drücken.</string>
+ <string name="crop_label">Bild zuschneiden</string>
+ <string name="crop_save_text">Speichern</string>
+ <string name="default_value_pref_gallery_size">1</string>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+ <string name="default_value_pref_gallery_sort">Absteigend</string>
+ <string name="delete">Löschen</string>
+ <string name="deny_delete">Abbrechen"</string>
+ <string name="details">Details</string>
+ <string name="details_category_label">Kategorie:</string>
+ <string name="details_date_taken">Aufnahmedatum:</string>
+ <string name="details_description_label">Beschreibung:</string>
+ <string name="details_description_text">Testbeschreibung</string>
+ <string name="details_file_size">Dateigröße:</string>
+ <string name="details_image_resolution">Auflösung:</string>
+ <string name="details_language_label">Sprache:</string>
+ <string name="details_panel_title">Details</string>
+ <string name="details_privateView_text">Privat</string>
+ <string name="details_publicView_text">Öffentlich</string>
+ <string name="details_save_text">Speichern</string>
+ <string name="details_tags_label">Tags:</string>
+ <string name="details_tags_text">Monster</string>
+ <string name="details_title_label">Titel:</string>
+ <string name="details_title_text">Eigenes Video</string>
+ <string name="details_uploaded_text">Erfolgreich hochgeladen</string>
+ <string name="edit">Bearbeiten</string>
+ <string name="flip_orientation">Ausrichtung wechseln</string>
+ <string name="gallery_label">Bilder</string>
+ <string name="gallery_large">Groß</string>
+ <string name="gallery_picker_label">Bilder</string>
+ <string name="gallery_small">Klein</string>
+ <string name="gallerysettings_duration">Dauer jedes Bildes in Bildschirmpräsentationen</string>
+ <string name="gallerysettings_speed1">1 Sekunde</string>
+ <string name="gallerysettings_speed2">3 Sekunden</string>
+ <string name="gallerysettings_speed3">5 Sekunden</string>
+ <string name="high">Hoch</string>
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> Bilder</string>
+ <string name="image_gallery_NoImageView_text">Keine Bilder gefunden.</string>
+ <string name="image_gallery_picker_images">Bilder</string>
+ <string name="image_gallery_picker_videos">Videos</string>
+ <string name="loading_progress_format_string"><xliff:g id="counter">%d</xliff:g> verbleiben</string>
+ <string name="low">Niedrig</string>
+ <string name="low_res">Niedrig</string>
+ <string name="med">Mittel</string>
+ <string name="medium_res">Mittel</string>
+ <string name="multiface_crop_help">Tippen Sie auf ein Gesicht, um zu beginnen.</string>
+ <string name="no">Nein</string>
+ <string name="no_images">Ihr Album ist leer.</string>
+ <string name="no_storage">Setzen Sie eine SD-Karte ein, bevor Sie die Kamera verwenden.</string>
+ <string name="no_way_to_share_image">Dieses Bild kann nicht gespeichert werden.</string>
+ <string name="no_way_to_share_video">Dieses Video kann nicht freigegeben werden.</string>
+ <string name="not_enough_space">Ihre SD-Karte ist voll.</string>
+ <string name="ok">OK</string>
+ <string name="photos_gallery_title">Bilder</string>
+ <string name="picasa_upload_label">Nach Picasa hochladen</string>
+ <string name="pick_photos_gallery_title">Bild auswählen</string>
+ <string name="pick_videos_gallery_title">Video auswählen</string>
+ <string name="pref_camera_autoupload_summary">Bilder automatisch nach Picasa hochladen</string>
+ <string name="pref_camera_autoupload_title">Bilder autom. hochladen</string>
+ <string name="pref_camera_category">Kamera</string>
+ <string name="pref_camera_postpicturemenu_summary">Aktionenmenü (Speichern, Löschen, ...) nach Aufnahme anzeigen</string>
+ <string name="pref_camera_postpicturemenu_title">Hinweis nach Aufnahme</string>
+ <string name="pref_camera_recordlocation_summary">Ort in Bilddaten aufnehmen</string>
+ <string name="pref_camera_recordlocation_title">Ort in Bildern speichern</string>
+ <string name="pref_camera_shuttersound_summary">Signalton bei Aufnahme eines Bildes</string>
+ <string name="pref_camera_shuttersound_title">Auslöserton abspielen</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Picasa-Albumname</string>
+ <string name="pref_camera_upload_albumname_summary">Zielalbum für Bilder benennen (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_title">Picasa-Albumname</string>
+ <string name="pref_gallery_category">Allgemeine Einstellungen</string>
+ <string name="pref_gallery_confirm_delete_summary">Bestätigung vor Löschen von Bildern anzeigen</string>
+ <string name="pref_gallery_confirm_delete_title">Löschen bestätigen</string>
+ <string name="pref_gallery_size_dialogtitle">Bildgröße</string>
+ <string name="pref_gallery_size_summary">Anzeigegröße für Bilder auswählen</string>
+ <string name="pref_gallery_size_title">Bildgröße</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Intervall Diaschau</string>
+ <string name="pref_gallery_slideshow_interval_summary">Bestimmen Sie, wie lange jedes Bild in der Diaschau angezeigt wird</string>
+ <string name="pref_gallery_slideshow_interval_title">Intervall Diaschau</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Diaschau wiederholen?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Diaschau mehr als einmal abspielen</string>
+ <string name="pref_gallery_slideshow_repeat_title">Diaschau wiederholen</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Bilder zufällig wiedergeben?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Bilder in beliebiger Reihenfolge anzeigen</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Bilder zufällig wiedergeben</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Diaschauübergang</string>
+ <string name="pref_gallery_slideshow_transition_summary">Wählen Sie den Effekt aus, der für den Übergang von einem Bild zum nächsten verwendet werden soll</string>
+ <string name="pref_gallery_slideshow_transition_title">Diaschauübergang</string>
+ <string name="pref_gallery_sort_dialogtitle">Bilder sortieren</string>
+ <string name="pref_gallery_sort_summary">Sortiermethode für Bilder auswählen</string>
+ <string name="pref_gallery_sort_title">Bilder sortieren</string>
+ <string name="pref_slideshow_category">Einstellungen Diaschau</string>
+ <string name="preferences_label">Kameraeinstellungen</string>
+ <string name="preview">Vorschau</string>
+ <string name="record">Aufnehmen</string>
+ <string name="rotate">Drehen</string>
+ <string name="rotate_left">Links drehen</string>
+ <string name="rotate_right">Rechts drehen</string>
+ <string name="runningFaceDetection">Bitte warten\u2026</string>
+ <string name="save">Speichern</string>
+ <string name="savingImage">Bild speichern\u2026</string>
+ <string name="sec1">1 Sek.</string>
+ <string name="sec2">2 Sek.</string>
+ <string name="sec3">3 Sek.</string>
+ <string name="sec4">4 Sek.</string>
+ <string name="sec5">5 Sek.</string>
+ <string name="sendImage">Bild freigeben über</string>
+ <string name="sendVideo">Video freigeben über</string>
+ <string name="setImage">Bild einstellen als</string>
+ <string name="set_wallpaper">Als Bildschirmhintergrund einstellen</string>
+ <string name="settings">Einstellungen</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Diaschau</string>
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+ <string name="stop">Anhalten</string>
+ <string name="testexif">Exif testen</string>
+ <string name="upload">Hochladen</string>
+ <string name="upload_all">Alle hochladen</string>
+ <string name="upload_default_category_text">Unterhaltung</string>
+ <string name="upload_default_description_text">Beschreibung hier eingeben</string>
+ <string name="upload_default_tags_text">Video</string>
+ <string name="upload_dialog_title">Nach YouTube hochladen - Videodetails eingeben</string>
+ <string name="upload_info_category">Kategorie:</string>
+ <string name="upload_info_description">Beschreibung:</string>
+ <string name="upload_info_private">Wenn dies nicht markiert ist, können nur Sie den Film sehen</string>
+ <string name="upload_info_tags">Tags:</string>
+ <string name="upload_info_title">Titel:</string>
+ <string name="upload_info_upload">Hochladen</string>
+ <string name="uploadingNPhotos"><xliff:g id="counter">%d</xliff:g> Bilder werden hochgeladen</string>
+ <string name="uploadingNVideos"><xliff:g id="counter">%d</xliff:g> Videos werden hochgeladen</string>
+ <string name="uploading_photos">Bilder werden hochgeladen</string>
+ <string name="uploading_photos_2">Bilder werden hochgeladen</string>
+ <string name="uploading_videos">Videos werden hochgeladen</string>
+ <string name="uploading_videos_2">Videos werden hochgeladen</string>
+ <string name="video_gallery_NoImageView_text">Keine Videos gefunden.</string>
+ <string name="video_play">Abspielen</string>
+ <string name="videos_gallery_title">Videos</string>
+ <string name="view">Ansicht</string>
+ <string name="view_label">Bild anzeigen</string>
+ <string name="view_video_label">Video anzeigen</string>
+ <string name="viewimage_comments_label_text">Kommentare</string>
+ <string name="viewimage_datetaken_label_text">Datum</string>
+ <string name="viewimage_digitalzoom_label_text">Digitaler Zoom</string>
+ <string name="viewimage_flash_label_text">Blitz benutzt</string>
+ <string name="viewimage_gps_latitude_label_text">GPS-Längengrad</string>
+ <string name="viewimage_gps_longitude_label_text">GPS-Längengrad</string>
+ <string name="viewimage_make_label_text">Erstellen</string>
+ <string name="viewimage_model_label_text">Von</string>
+ <string name="viewimage_orientation_label_text">Ausrichtung</string>
+ <string name="viewimage_res_label_text">Auflösung</string>
+ <string name="viewimage_status_text">Zum Zoomen Eingabe oder Leertaste drücken.</string>
+ <string name="wait">Bitte warten\u2026</string>
+ <string name="wallpaper">Bildschirmhintergrund wird eingestellt; bitte warten\u2026</string>
+ <string name="yes">Ja</string>
+ <string name="youtube_upload_label">Nach YouTube hochladen</string>
+ <string name="zoom">Zoom</string>
+</resources>
diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..d494400
--- /dev/null
+++ b/res/values-es-rUS/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="all_images">Todas las imágenes</string>
+ <string name="autocrop_before_query">¿Recortar antes?</string>
+ <string name="autocrop_no">Sí</string>
+ <string name="autocrop_yes">Sí</string>
+ <string name="best_res">Alta</string>
+ <string name="camera_NoStorageView_text">Inserte una tarjeta SD antes de utilizar la cámara.</string>
+ <string name="camera_ZoomIn_text">Acercar</string>
+ <string name="camera_ZoomOut_text">Alejar</string>
+ <string name="camera_button_hint">Pulse el botón Capturar para sacar una foto.</string>
+ <string name="camera_crop">Recortar</string>
+ <string name="camera_done">Nueva imagen</string>
+ <string name="camera_flash_auto">AutoFlash</string>
+ <string name="camera_flash_off">Flash desactivado</string>
+ <string name="camera_flash_on">Flash activado</string>
+ <string name="camera_flash_setting">Configuración del flash</string>
+ <string name="camera_gallery_photos_text">Imágenes</string>
+ <string name="camera_gallery_videos_text">Álbum de vídeos</string>
+ <string name="camera_keep">Guardar</string>
+ <string name="camera_keeping">Guardando\u2026</string>
+ <string name="camera_label">Cámara</string>
+ <string name="camera_mode_text">Modo</string>
+ <string name="camera_movie_record_counter_text">01:30:00 a.m.</string>
+ <string name="camera_pick_wallpaper">Imágenes</string>
+ <string name="camera_playvideo">Reproducir</string>
+ <string name="camera_record_text">Grabar</string>
+ <string name="camera_selectphoto">Seleccionar esta imagen</string>
+ <string name="camera_selectvideo">Seleccionar este vídeo</string>
+ <string name="camera_set">Establecer como</string>
+ <string name="camera_setas_contact">Foto del contacto</string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Papel tapiz</string>
+ <string name="camera_setas_wallpaper_drm">Imágenes adquiridas</string>
+ <string name="camera_share">Compartir</string>
+ <string name="camera_shareby_email">Correo electrónico</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (todas las imágenes)</string>
+ <string name="camera_switch_to_photo">Cambiar a imagen</string>
+ <string name="camera_switch_to_video">Cambiar a vídeo</string>
+ <string name="camera_takenewphoto">Capturar nueva imagen</string>
+ <string name="camera_takenewvideo">Capturar nuevo vídeo</string>
+ <string name="camera_toss">Eliminar</string>
+ <string name="camera_tossing">Eliminando\u2026</string>
+ <string name="camera_zoom_1_text">Zoom 1,3x</string>
+ <string name="camera_zoom_2_text">Zoom 2x</string>
+ <string name="camera_zoom_3_text">Zoom 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">Configuración</string>
+ <string name="camerasettings_autoupload_label">Cargar automáticamente imágenes en la Web</string>
+ <string name="camerasettings_done">Listo</string>
+ <string name="camerasettings_duration_label">Duración:</string>
+ <string name="camerasettings_image_quality_label">Calidad de la imagen:</string>
+ <string name="camerasettings_no">No</string>
+ <string name="camerasettings_off">Desactivado</string>
+ <string name="camerasettings_on">Activado</string>
+ <string name="camerasettings_play_click_sound_label">Sonido del disparador</string>
+ <string name="camerasettings_preferred_view_label">Vista preferida:</string>
+ <string name="camerasettings_resolution_label">Resolución:</string>
+ <string name="camerasettings_test2_label">prueba2:</string>
+ <string name="camerasettings_test3_label">prueba3:</string>
+ <string name="camerasettings_test4_label">prueba4:</string>
+ <string name="camerasettings_use_sd_card_label">Guardar imágenes en tarjeta SD</string>
+ <string name="camerasettings_yes">Sí</string>
+ <string name="cancel">Cancelar</string>
+ <string name="confirm_delete">Confirmar eliminación</string>
+ <string name="confirm_delete_message">Se eliminará la imagen.</string>
+ <string name="confirm_delete_title">Eliminar</string>
+ <string name="context_menu_header">Opciones de imagen</string>
+ <string name="crop_discard_text">Rechazar</string>
+ <string name="crop_help">Pulsar ALT para cambiar el tamaño.</string>
+ <string name="crop_label">Recortar imagen</string>
+ <string name="crop_save_text">Guardar</string>
+ <string name="default_value_pref_gallery_size">1</string>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+ <string name="default_value_pref_gallery_sort">descendente</string>
+ <string name="delete">Eliminar</string>
+ <string name="deny_delete">Cancelar"</string>
+ <string name="details">Detalles</string>
+ <string name="details_category_label">Categoría:</string>
+ <string name="details_date_taken">Fecha de captura:</string>
+ <string name="details_description_label">Descripción:</string>
+ <string name="details_description_text">descripción de prueba</string>
+ <string name="details_file_size">Tamaño de archivo:</string>
+ <string name="details_image_resolution">Resolución:</string>
+ <string name="details_language_label">Idioma:</string>
+ <string name="details_panel_title">Detalles</string>
+ <string name="details_privateView_text">Privado</string>
+ <string name="details_publicView_text">Público</string>
+ <string name="details_save_text">Guardar</string>
+ <string name="details_tags_label">Etiquetas:</string>
+ <string name="details_tags_text">monstruo</string>
+ <string name="details_title_label">Título:</string>
+ <string name="details_title_text">mivídeo</string>
+ <string name="details_uploaded_text">Cargado correctamente</string>
+ <string name="edit">Editar</string>
+ <string name="flip_orientation">Orientación de volteo</string>
+ <string name="gallery_label">Imágenes</string>
+ <string name="gallery_large">Grande</string>
+ <string name="gallery_picker_label">Imágenes</string>
+ <string name="gallery_small">Pequeño</string>
+ <string name="gallerysettings_duration">Duración de cada diapositiva en las presentaciones</string>
+ <string name="gallerysettings_speed1">1 segundo</string>
+ <string name="gallerysettings_speed2">3 segundos</string>
+ <string name="gallerysettings_speed3">5 segundos</string>
+ <string name="high">Alta</string>
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> imágenes</string>
+ <string name="image_gallery_NoImageView_text">Ninguna imagen encontrada.</string>
+ <string name="image_gallery_picker_images">Imágenes</string>
+ <string name="image_gallery_picker_videos">Vídeos</string>
+ <string name="loading_progress_format_string"><xliff:g id="counter">%d</xliff:g> restante(s)</string>
+ <string name="low">Baja</string>
+ <string name="low_res">Baja</string>
+ <string name="med">Medio</string>
+ <string name="medium_res">Medio</string>
+ <string name="multiface_crop_help">Puntee en una cara para empezar.</string>
+ <string name="no">No</string>
+ <string name="no_images">Su álbum está vacío.</string>
+ <string name="no_storage">Inserte una tarjeta SD antes de utilizar la cámara.</string>
+ <string name="no_way_to_share_image">Esta imagen no se puede guardar.</string>
+ <string name="no_way_to_share_video">Este vídeo no se puede compartir.</string>
+ <string name="not_enough_space">Su tarjeta SD está llena.</string>
+ <string name="ok">Aceptar</string>
+ <string name="photos_gallery_title">Imágenes</string>
+ <string name="picasa_upload_label">Carga de Picasa</string>
+ <string name="pick_photos_gallery_title">Seleccionar imagen</string>
+ <string name="pick_videos_gallery_title">Seleccionar vídeo</string>
+ <string name="pref_camera_autoupload_summary">Cargar imágenes automáticamente en Picasa</string>
+ <string name="pref_camera_autoupload_title">Cargar imágenes automáticamente</string>
+ <string name="pref_camera_category">Cámara</string>
+ <string name="pref_camera_postpicturemenu_summary">Mostrar el menú de acción (guardar, eliminar, etc.) después de capturar</string>
+ <string name="pref_camera_postpicturemenu_title">Preguntar después de la captura</string>
+ <string name="pref_camera_recordlocation_summary">Registrar ubicación en datos de imagen</string>
+ <string name="pref_camera_recordlocation_title">Guardar ubicación en imágenes</string>
+ <string name="pref_camera_shuttersound_summary">Reproducir un sonido al hacer una foto</string>
+ <string name="pref_camera_shuttersound_title">Reproducir sonido del disparador</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Nombre de álbum de Picasa</string>
+ <string name="pref_camera_upload_albumname_summary">Asigne un nombre al álbum de destino de sus imágenes (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_title">Nombre de álbum de Picasa</string>
+ <string name="pref_gallery_category">Configuración generales</string>
+ <string name="pref_gallery_confirm_delete_summary">Mostrar confirmación antes de eliminar imágenes</string>
+ <string name="pref_gallery_confirm_delete_title">Confirmar eliminaciones</string>
+ <string name="pref_gallery_size_dialogtitle">Tamaño de imagen</string>
+ <string name="pref_gallery_size_summary">Seleccione el tamaño de visualización de las imágenes</string>
+ <string name="pref_gallery_size_title">Tamaño de imagen</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Intervalo en presentación de diapositivas</string>
+ <string name="pref_gallery_slideshow_interval_summary">Seleccione el intervalo entre diapositiva y diapositiva en la presentación</string>
+ <string name="pref_gallery_slideshow_interval_title">Intervalo en presentación de diapositivas</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">¿Repetir presentación de diapositivas?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Reproduzca la presentación de diapositivas más de una vez</string>
+ <string name="pref_gallery_slideshow_repeat_title">Repetir presentación de diapositivas</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">¿Mostrar imágenes aleatoriamente?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Mostrar imágenes en orden aleatorio</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Mostrar diapositivas aleatoriamente</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Transición de presentación de diapositivas</string>
+ <string name="pref_gallery_slideshow_transition_summary">Seleccione el efecto que desea utilizar al pasar de una diapositiva a otra</string>
+ <string name="pref_gallery_slideshow_transition_title">Transición de presentación de diapositivas</string>
+ <string name="pref_gallery_sort_dialogtitle">Orden de las imágenes</string>
+ <string name="pref_gallery_sort_summary">Seleccionar el orden de clasificación de las imágenes</string>
+ <string name="pref_gallery_sort_title">Orden de las imágenes</string>
+ <string name="pref_slideshow_category">Configuración de la presentación de diapositivas</string>
+ <string name="preferences_label">Configuración de la cámara</string>
+ <string name="preview">Vista previa</string>
+ <string name="record">Grabar</string>
+ <string name="rotate">Girar</string>
+ <string name="rotate_left">Girar a la izquierda</string>
+ <string name="rotate_right">Girar a la derecha</string>
+ <string name="runningFaceDetection">Por favor, espere\u2026</string>
+ <string name="save">Guardar</string>
+ <string name="savingImage">Guardando imagen\u2026</string>
+ <string name="sec1">1 s</string>
+ <string name="sec2">2 s</string>
+ <string name="sec3">3 s</string>
+ <string name="sec4">4 s</string>
+ <string name="sec5">5 s</string>
+ <string name="sendImage">Compartir imagen por</string>
+ <string name="sendVideo">Compartir vídeo por</string>
+ <string name="setImage">Establecer imagen como</string>
+ <string name="set_wallpaper">Establecer como papel tapiz</string>
+ <string name="settings">Configuración</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Presentación de diapositivas</string>
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+ <string name="stop">Detener</string>
+ <string name="testexif">Prueba de Exif</string>
+ <string name="upload">Cargar</string>
+ <string name="upload_all">Cargar todo</string>
+ <string name="upload_default_category_text">Entretenimiento</string>
+ <string name="upload_default_description_text">aquí va la descripción</string>
+ <string name="upload_default_tags_text">vídeo</string>
+ <string name="upload_dialog_title">Cargar en YouTube - introducir detalles del vídeo</string>
+ <string name="upload_info_category">Categoría:</string>
+ <string name="upload_info_description">Descripción:</string>
+ <string name="upload_info_private">Si esta opción no está activada, sólo podrá ver el vídeo usted</string>
+ <string name="upload_info_tags">Etiquetas:</string>
+ <string name="upload_info_title">Título:</string>
+ <string name="upload_info_upload">Cargar</string>
+ <string name="uploadingNPhotos">Cargando <xliff:g id="counter">%d</xliff:g> imágenes</string>
+ <string name="uploadingNVideos">Cargando <xliff:g id="counter">%d</xliff:g> vídeos</string>
+ <string name="uploading_photos">Cargando imágenes</string>
+ <string name="uploading_photos_2">Cargando imágenes</string>
+ <string name="uploading_videos">Cargando vídeos</string>
+ <string name="uploading_videos_2">Cargando vídeos</string>
+ <string name="video_gallery_NoImageView_text">No se ha encontrado ningún vídeo.</string>
+ <string name="video_play">Reproducir</string>
+ <string name="videos_gallery_title">Vídeos</string>
+ <string name="view">Ver</string>
+ <string name="view_label">Ver imagen</string>
+ <string name="view_video_label">Ver vídeo</string>
+ <string name="viewimage_comments_label_text">Comentarios</string>
+ <string name="viewimage_datetaken_label_text">Fecha</string>
+ <string name="viewimage_digitalzoom_label_text">Zoom digital</string>
+ <string name="viewimage_flash_label_text">Flash usado</string>
+ <string name="viewimage_gps_latitude_label_text">Latitud GPS</string>
+ <string name="viewimage_gps_longitude_label_text">Longitud GPS</string>
+ <string name="viewimage_make_label_text">Hacer</string>
+ <string name="viewimage_model_label_text">De</string>
+ <string name="viewimage_orientation_label_text">Orientación</string>
+ <string name="viewimage_res_label_text">Resolución</string>
+ <string name="viewimage_status_text">Pulse Intro o la barra espaciadora para utilizar el zoom.</string>
+ <string name="wait">Por favor, espere\u2026</string>
+ <string name="wallpaper">Estableciendo papel tapiz. Por favor, espere\u2026</string>
+ <string name="yes">Sí</string>
+ <string name="youtube_upload_label">Carga en YouTube</string>
+ <string name="zoom">Zoom</string>
+</resources>
diff --git a/res/values-fr-rFR/strings.xml b/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000..20aa1a7
--- /dev/null
+++ b/res/values-fr-rFR/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="all_images">Toutes les images</string>
+ <string name="autocrop_before_query">Rogner avant ?</string>
+ <string name="autocrop_no">Oui</string>
+ <string name="autocrop_yes">Oui</string>
+ <string name="best_res">Haut</string>
+ <string name="camera_NoStorageView_text">Veuillez insérer une carte SD avant d\'utiliser l\'appareil photo.</string>
+ <string name="camera_ZoomIn_text">Zoom avant</string>
+ <string name="camera_ZoomOut_text">Zoom arrière</string>
+ <string name="camera_button_hint">Appuyez le bouton Capture pour prendre la photo.</string>
+ <string name="camera_crop">Rogner</string>
+ <string name="camera_done">Nouvelle image</string>
+ <string name="camera_flash_auto">FlashAuto</string>
+ <string name="camera_flash_off">Flash ACT</string>
+ <string name="camera_flash_on">Flash DÉS</string>
+ <string name="camera_flash_setting">Paramètre du flash</string>
+ <string name="camera_gallery_photos_text">Images</string>
+ <string name="camera_gallery_videos_text">Album vidéo</string>
+ <string name="camera_keep">Enregistrer</string>
+ <string name="camera_keeping">Conserver\u2026</string>
+ <string name="camera_label">Appareil photo</string>
+ <string name="camera_mode_text">Mode</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_pick_wallpaper">Images</string>
+ <string name="camera_playvideo">Lecture</string>
+ <string name="camera_record_text">Enregistrer</string>
+ <string name="camera_selectphoto">Sélectionner cette image</string>
+ <string name="camera_selectvideo">Sélectionner cette vidéo</string>
+ <string name="camera_set">Définir comme</string>
+ <string name="camera_setas_contact">Image de contact</string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Papier peint</string>
+ <string name="camera_setas_wallpaper_drm">Images achetées</string>
+ <string name="camera_share">Partager</string>
+ <string name="camera_shareby_email">Email</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (toutes les images)</string>
+ <string name="camera_switch_to_photo">Passer en image</string>
+ <string name="camera_switch_to_video">Passer en vidéo</string>
+ <string name="camera_takenewphoto">Capture nouvelle image</string>
+ <string name="camera_takenewvideo">Capture nouvelle vidéo</string>
+ <string name="camera_toss">Supprimer</string>
+ <string name="camera_tossing">Suppression\u2026</string>
+ <string name="camera_zoom_1_text">Zoom x1,3</string>
+ <string name="camera_zoom_2_text">Zoom x2</string>
+ <string name="camera_zoom_3_text">Zoom x4</string>
+ <string name="camera_zoom_normal_text">x1</string>
+ <string name="camerasettings">Paramètres</string>
+ <string name="camerasettings_autoupload_label">Transférer automatiquement les images vers le Web</string>
+ <string name="camerasettings_done">Terminé</string>
+ <string name="camerasettings_duration_label">Durée :</string>
+ <string name="camerasettings_image_quality_label">Qualité d\'image :</string>
+ <string name="camerasettings_no">Non</string>
+ <string name="camerasettings_off">Désactivé</string>
+ <string name="camerasettings_on">Activé</string>
+ <string name="camerasettings_play_click_sound_label">Son du déclencheur</string>
+ <string name="camerasettings_preferred_view_label">Vue préférée :</string>
+ <string name="camerasettings_resolution_label">Résolution :</string>
+ <string name="camerasettings_test2_label">test2 :</string>
+ <string name="camerasettings_test3_label">test3 :</string>
+ <string name="camerasettings_test4_label">test4 :</string>
+ <string name="camerasettings_use_sd_card_label">Enregistrer les images sur la carte SD</string>
+ <string name="camerasettings_yes">Oui</string>
+ <string name="cancel">Annuler</string>
+ <string name="confirm_delete">Confirmer la suppression</string>
+ <string name="confirm_delete_message">L\'image sera supprimée.</string>
+ <string name="confirm_delete_title">Supprimer</string>
+ <string name="context_menu_header">Options d\'image</string>
+ <string name="crop_discard_text">Abandonner</string>
+ <string name="crop_help">Maintenez ALT pour redimensionner.</string>
+ <string name="crop_label">Rogner l\'image</string>
+ <string name="crop_save_text">Enregistrer</string>
+ <string name="default_value_pref_gallery_size">1</string>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+ <string name="default_value_pref_gallery_sort">décroissant</string>
+ <string name="delete">Supprimer</string>
+ <string name="deny_delete">Annuler"</string>
+ <string name="details">Détails</string>
+ <string name="details_category_label">Catégorie :</string>
+ <string name="details_date_taken">Date de prise :</string>
+ <string name="details_description_label">Description :</string>
+ <string name="details_description_text">descriptiontest</string>
+ <string name="details_file_size">Taille du fichier :</string>
+ <string name="details_image_resolution">Résolution :</string>
+ <string name="details_language_label">Langue :</string>
+ <string name="details_panel_title">Détails</string>
+ <string name="details_privateView_text">Privé</string>
+ <string name="details_publicView_text">Publique</string>
+ <string name="details_save_text">Enregistrer</string>
+ <string name="details_tags_label">Tags :</string>
+ <string name="details_tags_text">monstre</string>
+ <string name="details_title_label">Titre :</string>
+ <string name="details_title_text">mavidéo</string>
+ <string name="details_uploaded_text">Transfert réussi</string>
+ <string name="edit">Modifier</string>
+ <string name="flip_orientation">Retourner l\'orientation</string>
+ <string name="gallery_label">Images</string>
+ <string name="gallery_large">Grande</string>
+ <string name="gallery_picker_label">Images</string>
+ <string name="gallery_small">Petit</string>
+ <string name="gallerysettings_duration">Durée de chaque diapo dans les diaporamas</string>
+ <string name="gallerysettings_speed1">1 seconde</string>
+ <string name="gallerysettings_speed2">3 secondes</string>
+ <string name="gallerysettings_speed3">5 secondes</string>
+ <string name="high">Haut</string>
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> images</string>
+ <string name="image_gallery_NoImageView_text">Aucune image trouvée.</string>
+ <string name="image_gallery_picker_images">Images</string>
+ <string name="image_gallery_picker_videos">Vidéos</string>
+ <string name="loading_progress_format_string"><xliff:g id="counter">%d</xliff:g> restant</string>
+ <string name="low">Bas</string>
+ <string name="low_res">Bas</string>
+ <string name="med">Moyen</string>
+ <string name="medium_res">Moyen</string>
+ <string name="multiface_crop_help">Appuyez un visage pour commencer.</string>
+ <string name="no">Non</string>
+ <string name="no_images">Votre album est vide.</string>
+ <string name="no_storage">Veuillez insérer une carte SD avant d\'utiliser l\'appareil photo.</string>
+ <string name="no_way_to_share_image">Cette image ne peut pas être enregistrée.</string>
+ <string name="no_way_to_share_video">Cette vidéo ne peut pas être partagée.</string>
+ <string name="not_enough_space">Votre carte SD est pleine.</string>
+ <string name="ok">OK</string>
+ <string name="photos_gallery_title">Images</string>
+ <string name="picasa_upload_label">Transfert Picasa</string>
+ <string name="pick_photos_gallery_title">Sélectionner une image</string>
+ <string name="pick_videos_gallery_title">Sélectionner une vidéo</string>
+ <string name="pref_camera_autoupload_summary">Transférer les images à Picasa automatiquement</string>
+ <string name="pref_camera_autoupload_title">Transfert auto des images</string>
+ <string name="pref_camera_category">Appareil photo</string>
+ <string name="pref_camera_postpicturemenu_summary">Afficher le menu action (enregistrer, supprimer, ...) après la capture</string>
+ <string name="pref_camera_postpicturemenu_title">Inviter après la capture</string>
+ <string name="pref_camera_recordlocation_summary">Enregistrer le lieu dans les données d\'image</string>
+ <string name="pref_camera_recordlocation_title">Stocker le lieu dans les images</string>
+ <string name="pref_camera_shuttersound_summary">Émettre un son à la prise d\'une photo</string>
+ <string name="pref_camera_shuttersound_title">Émettre le son du déclencheur</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Nom d\'album Picasa</string>
+ <string name="pref_camera_upload_albumname_summary">Nommez l\'album de destination pour vos images (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_title">Nom d\'album Picasa</string>
+ <string name="pref_gallery_category">Paramètres généraux</string>
+ <string name="pref_gallery_confirm_delete_summary">Afficher la confirmation avant de supprimer les images</string>
+ <string name="pref_gallery_confirm_delete_title">Confirmer les suppressions</string>
+ <string name="pref_gallery_size_dialogtitle">Taille d\'image</string>
+ <string name="pref_gallery_size_summary">Sélectionnez la taille d\'affichage des images</string>
+ <string name="pref_gallery_size_title">Taille d\'image</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Intervalle du diaporama</string>
+ <string name="pref_gallery_slideshow_interval_summary">Sélectionnez la durée d\'affichage de chaque diapo dans le diaporama</string>
+ <string name="pref_gallery_slideshow_interval_title">Intervalle du diaporama</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Répéter le diaporama ?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Lire le diaporama plus d\'une fois</string>
+ <string name="pref_gallery_slideshow_repeat_title">Répéter le diaporama</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Mélanger les images ?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Afficher les images en ordre aléatoire</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Mélanger les diapos</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Transition du diaporama</string>
+ <string name="pref_gallery_slideshow_transition_summary">Sélectionner l\'effet utilisé lors du passage d\'une diapo à la suivante</string>
+ <string name="pref_gallery_slideshow_transition_title">Transition du diaporama</string>
+ <string name="pref_gallery_sort_dialogtitle">Tri des images</string>
+ <string name="pref_gallery_sort_summary">Sélectionnez l\'ordre de tri des images</string>
+ <string name="pref_gallery_sort_title">Tri des images</string>
+ <string name="pref_slideshow_category">Paramètres du diaporama</string>
+ <string name="preferences_label">Paramètres de l\'appareil photo</string>
+ <string name="preview">Aperçu</string>
+ <string name="record">Enregistrer</string>
+ <string name="rotate">Faire pivoter</string>
+ <string name="rotate_left">Pivoter à gauche</string>
+ <string name="rotate_right">Pivoter à droite</string>
+ <string name="runningFaceDetection">Veuillez attendre\u2026</string>
+ <string name="save">Enregistrer</string>
+ <string name="savingImage">Enregistrement de l\'image\u2026</string>
+ <string name="sec1">1 s</string>
+ <string name="sec2">2 s</string>
+ <string name="sec3">3 s</string>
+ <string name="sec4">4 s</string>
+ <string name="sec5">5 s</string>
+ <string name="sendImage">Partager l\'image via</string>
+ <string name="sendVideo">Partager la vidéo via</string>
+ <string name="setImage">Définir l\'image comme</string>
+ <string name="set_wallpaper">Définir comme papier peint</string>
+ <string name="settings">Paramètres</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Diaporama</string>
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+ <string name="stop">Arrêter</string>
+ <string name="testexif">Test Exif</string>
+ <string name="upload">Transférer</string>
+ <string name="upload_all">Transférer tout</string>
+ <string name="upload_default_category_text">Divertissement</string>
+ <string name="upload_default_description_text">la description ici</string>
+ <string name="upload_default_tags_text">vidéo</string>
+ <string name="upload_dialog_title">Transférer à YouTube - saisissez les détails de la vidéo</string>
+ <string name="upload_info_category">Catégorie :</string>
+ <string name="upload_info_description">Description :</string>
+ <string name="upload_info_private">Si désélectionné, seul vous pouvez voir le film</string>
+ <string name="upload_info_tags">Tags :</string>
+ <string name="upload_info_title">Titre :</string>
+ <string name="upload_info_upload">Transférer</string>
+ <string name="uploadingNPhotos">Transfert de <xliff:g id="counter">%d</xliff:g> images</string>
+ <string name="uploadingNVideos">Transfert de <xliff:g id="counter">%d</xliff:g> vidéos</string>
+ <string name="uploading_photos">Transfert des images</string>
+ <string name="uploading_photos_2">Transfert des images</string>
+ <string name="uploading_videos">Transfert des vidéos</string>
+ <string name="uploading_videos_2">Transfert des vidéos</string>
+ <string name="video_gallery_NoImageView_text">Aucune vidéo trouvée.</string>
+ <string name="video_play">Lecture</string>
+ <string name="videos_gallery_title">Vidéos</string>
+ <string name="view">Afficher</string>
+ <string name="view_label">Afficher l\'image</string>
+ <string name="view_video_label">Afficher la vidéo</string>
+ <string name="viewimage_comments_label_text">Commentaires</string>
+ <string name="viewimage_datetaken_label_text">Date</string>
+ <string name="viewimage_digitalzoom_label_text">Zoom numérique</string>
+ <string name="viewimage_flash_label_text">Flash utilisé</string>
+ <string name="viewimage_gps_latitude_label_text">Latitude GPS</string>
+ <string name="viewimage_gps_longitude_label_text">Longitude GPS</string>
+ <string name="viewimage_make_label_text">Faire</string>
+ <string name="viewimage_model_label_text">De</string>
+ <string name="viewimage_orientation_label_text">Orientation</string>
+ <string name="viewimage_res_label_text">Résolution</string>
+ <string name="viewimage_status_text">Appuyez Entrée ou Espace pour le zoom.</string>
+ <string name="wait">Veuillez attendre\u2026</string>
+ <string name="wallpaper">Définition du papier peint, veuillez patienter\u2026</string>
+ <string name="yes">Oui</string>
+ <string name="youtube_upload_label">Transfert YouTube</string>
+ <string name="zoom">Zoom</string>
+</resources>
diff --git a/res/values-it-rIT/strings.xml b/res/values-it-rIT/strings.xml
new file mode 100644
index 0000000..f82048c
--- /dev/null
+++ b/res/values-it-rIT/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="all_images">Tutte le immagini</string>
+ <string name="autocrop_before_query">Ritagliare prima?</string>
+ <string name="autocrop_no">Sì</string>
+ <string name="autocrop_yes">Sì</string>
+ <string name="best_res">Alta</string>
+ <string name="camera_NoStorageView_text">Inserire una scheda SD prima di utilizzare la fotocamera.</string>
+ <string name="camera_ZoomIn_text">Zoom avanti</string>
+ <string name="camera_ZoomOut_text">Zoom indietro</string>
+ <string name="camera_button_hint">Premere il pulsante Acquisisci per acquisire l'immagine.</string>
+ <string name="camera_crop">Ritaglia</string>
+ <string name="camera_done">Nuova immagine</string>
+ <string name="camera_flash_auto">AutoFlash</string>
+ <string name="camera_flash_off">Flash OFF</string>
+ <string name="camera_flash_on">Flash ON</string>
+ <string name="camera_flash_setting">Impostazione Flash</string>
+ <string name="camera_gallery_photos_text">Immagini</string>
+ <string name="camera_gallery_videos_text">Album video</string>
+ <string name="camera_keep">Salva</string>
+ <string name="camera_keeping">Mantenimento\u2026</string>
+ <string name="camera_label">Fotocamera</string>
+ <string name="camera_mode_text">Modalità</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_pick_wallpaper">Immagini</string>
+ <string name="camera_playvideo">Riproduci</string>
+ <string name="camera_record_text">Registra</string>
+ <string name="camera_selectphoto">Selezionare questa immagine</string>
+ <string name="camera_selectvideo">Selezionare questo video</string>
+ <string name="camera_set">Imposta come</string>
+ <string name="camera_setas_contact">Immagine contatto</string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Sfondo</string>
+ <string name="camera_setas_wallpaper_drm">Immagini acquistate</string>
+ <string name="camera_share">Condividi</string>
+ <string name="camera_shareby_email">Email</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (tutte le immagini)</string>
+ <string name="camera_switch_to_photo">Passa a immagine</string>
+ <string name="camera_switch_to_video">Passa a video</string>
+ <string name="camera_takenewphoto">Acquisisci nuova immagine</string>
+ <string name="camera_takenewvideo">Acquisisci nuovo video</string>
+ <string name="camera_toss">Elimina</string>
+ <string name="camera_tossing">Eliminazione in corso\u2026</string>
+ <string name="camera_zoom_1_text">Zoom 1,3x</string>
+ <string name="camera_zoom_2_text">Zoom 2x</string>
+ <string name="camera_zoom_3_text">Zoom 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">Impostazioni</string>
+ <string name="camerasettings_autoupload_label">Carica automaticamente immagini sul Web</string>
+ <string name="camerasettings_done">Completato</string>
+ <string name="camerasettings_duration_label">Durata:</string>
+ <string name="camerasettings_image_quality_label">Qualità immagine:</string>
+ <string name="camerasettings_no">No</string>
+ <string name="camerasettings_off">Disattivato</string>
+ <string name="camerasettings_on">Attivato</string>
+ <string name="camerasettings_play_click_sound_label">Suono otturatore</string>
+ <string name="camerasettings_preferred_view_label">Visualizzazione preferita:</string>
+ <string name="camerasettings_resolution_label">Risoluzione:</string>
+ <string name="camerasettings_test2_label">test2:</string>
+ <string name="camerasettings_test3_label">test3:</string>
+ <string name="camerasettings_test4_label">test4:</string>
+ <string name="camerasettings_use_sd_card_label">Salva immagine nella scheda SD</string>
+ <string name="camerasettings_yes">Sì</string>
+ <string name="cancel">Annulla</string>
+ <string name="confirm_delete">Conferma eliminazione</string>
+ <string name="confirm_delete_message">L'immagine verrà eliminata.</string>
+ <string name="confirm_delete_title">Elimina</string>
+ <string name="context_menu_header">Opzioni immagine</string>
+ <string name="crop_discard_text">Ignora</string>
+ <string name="crop_help">Tenere premuto ALT per ridimensionare.</string>
+ <string name="crop_label">Ritaglia immagine</string>
+ <string name="crop_save_text">Salva</string>
+ <string name="default_value_pref_gallery_size">1</string>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+ <string name="default_value_pref_gallery_sort">decrescente</string>
+ <string name="delete">Elimina</string>
+ <string name="deny_delete">Annulla"</string>
+ <string name="details">Dettagli</string>
+ <string name="details_category_label">Categoria:</string>
+ <string name="details_date_taken">Data scatto:</string>
+ <string name="details_description_label">Descrizione:</string>
+ <string name="details_description_text">descrizionetest</string>
+ <string name="details_file_size">Dimensione file:</string>
+ <string name="details_image_resolution">Risoluzione:</string>
+ <string name="details_language_label">Lingua:</string>
+ <string name="details_panel_title">Dettagli</string>
+ <string name="details_privateView_text">Privato</string>
+ <string name="details_publicView_text">Pubblico</string>
+ <string name="details_save_text">Salva</string>
+ <string name="details_tags_label">Tag:</string>
+ <string name="details_tags_text">monster</string>
+ <string name="details_title_label">Titolo:</string>
+ <string name="details_title_text">myvideo</string>
+ <string name="details_uploaded_text">Caricamento completato</string>
+ <string name="edit">Modifica</string>
+ <string name="flip_orientation">Capovolgi orientamento</string>
+ <string name="gallery_label">Immagini</string>
+ <string name="gallery_large">Grande</string>
+ <string name="gallery_picker_label">Immagini</string>
+ <string name="gallery_small">Piccolo</string>
+ <string name="gallerysettings_duration">Durata di ogni diapositiva nelle presentazioni</string>
+ <string name="gallerysettings_speed1">1 secondo</string>
+ <string name="gallerysettings_speed2">3 secondi</string>
+ <string name="gallerysettings_speed3">5 secondi</string>
+ <string name="high">Alta</string>
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> immagini</string>
+ <string name="image_gallery_NoImageView_text">Nessuna immagine trovata.</string>
+ <string name="image_gallery_picker_images">Immagini</string>
+ <string name="image_gallery_picker_videos">Video</string>
+ <string name="loading_progress_format_string"><xliff:g id="counter">%d</xliff:g> rimanente</string>
+ <string name="low">Bassa</string>
+ <string name="low_res">Bassa</string>
+ <string name="med">Supporto</string>
+ <string name="medium_res">Supporto</string>
+ <string name="multiface_crop_help">Toccare una faccia per iniziare.</string>
+ <string name="no">No</string>
+ <string name="no_images">L'album è vuoto.</string>
+ <string name="no_storage">Inserire una scheda SD prima di utilizzare la fotocamera.</string>
+ <string name="no_way_to_share_image">Impossibile salvare questa immagine.</string>
+ <string name="no_way_to_share_video">Impossibile condividere questo video.</string>
+ <string name="not_enough_space">La scheda SD è piena.</string>
+ <string name="ok">OK</string>
+ <string name="photos_gallery_title">Immagini</string>
+ <string name="picasa_upload_label">Caricamento Picasa</string>
+ <string name="pick_photos_gallery_title">Seleziona immagine</string>
+ <string name="pick_videos_gallery_title">Seleziona video</string>
+ <string name="pref_camera_autoupload_summary">Carica automaticamente le immagini in Picasa</string>
+ <string name="pref_camera_autoupload_title">Carica immagini automaticamente</string>
+ <string name="pref_camera_category">Fotocamera</string>
+ <string name="pref_camera_postpicturemenu_summary">Visualizza il menu Azione (salva. elimina ecc...) dopo l'acquisizione</string>
+ <string name="pref_camera_postpicturemenu_title">Prompt dopo acquisizione</string>
+ <string name="pref_camera_recordlocation_summary">Registra ubicazione nei dati immagine</string>
+ <string name="pref_camera_recordlocation_title">Memorizza posizione nelle immagini</string>
+ <string name="pref_camera_shuttersound_summary">Riproduci un suono durante l'acquisizione di un'immagine</string>
+ <string name="pref_camera_shuttersound_title">Riproduci suono otturatore</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Nome album Picasa</string>
+ <string name="pref_camera_upload_albumname_summary">Assegnare un nome all'album di destinazione delle immagini (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_title">Nome album Picasa</string>
+ <string name="pref_gallery_category">Impostazioni generali</string>
+ <string name="pref_gallery_confirm_delete_summary">Mostra conferma prima di eliminare le immagini</string>
+ <string name="pref_gallery_confirm_delete_title">Conferma eliminazioni</string>
+ <string name="pref_gallery_size_dialogtitle">Dimensione immagine</string>
+ <string name="pref_gallery_size_summary">Selezionare la dimensione di visualizzazione delle immagini</string>
+ <string name="pref_gallery_size_title">Dimensione immagine</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Intervallo presentazione</string>
+ <string name="pref_gallery_slideshow_interval_summary">Selezionare la durata di visualizzazione di ogni diapositiva nella presentazione</string>
+ <string name="pref_gallery_slideshow_interval_title">Intervallo presentazione</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Ripetere la presentazione?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Riproduci presentazione più volte</string>
+ <string name="pref_gallery_slideshow_repeat_title">Ripeti presentazione</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Riprodurre le immagini casualmente?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Mostra le immagini in ordine casuale</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Riproduzione casuale diapositive</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Transizione presentazione</string>
+ <string name="pref_gallery_slideshow_transition_summary">Scegliere l'effetto che si desidera utilizzare durante lo spostamento da una diapositiva all'altra</string>
+ <string name="pref_gallery_slideshow_transition_title">Transizione presentazione</string>
+ <string name="pref_gallery_sort_dialogtitle">Ordinamento immagini</string>
+ <string name="pref_gallery_sort_summary">Selezionare l'ordinamento delle immagini</string>
+ <string name="pref_gallery_sort_title">Ordinamento immagini</string>
+ <string name="pref_slideshow_category">Impostazioni presentazione</string>
+ <string name="preferences_label">Impostazioni fotocamera</string>
+ <string name="preview">Anteprima</string>
+ <string name="record">Registra</string>
+ <string name="rotate">Ruota</string>
+ <string name="rotate_left">Ruota a sinistra</string>
+ <string name="rotate_right">Ruota a destra</string>
+ <string name="runningFaceDetection">Attendere\u2026</string>
+ <string name="save">Salva</string>
+ <string name="savingImage">Salvataggio immagine\u2026</string>
+ <string name="sec1">1 sec</string>
+ <string name="sec2">2 sec</string>
+ <string name="sec3">3 sec</string>
+ <string name="sec4">4 sec</string>
+ <string name="sec5">5 sec</string>
+ <string name="sendImage">Condividi immagine via</string>
+ <string name="sendVideo">Condividi video via</string>
+ <string name="setImage">Imposta immagine come</string>
+ <string name="set_wallpaper">Imposta come sfondo</string>
+ <string name="settings">Impostazioni</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Presentazione</string>
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+ <string name="stop">Interrompi</string>
+ <string name="testexif">Test Exif</string>
+ <string name="upload">Carica</string>
+ <string name="upload_all">Carica tutto</string>
+ <string name="upload_default_category_text">Intrattenimento</string>
+ <string name="upload_default_description_text">campo per la descrizione</string>
+ <string name="upload_default_tags_text">video</string>
+ <string name="upload_dialog_title">Carica su YouTube - digitare i dettagli video</string>
+ <string name="upload_info_category">Categoria:</string>
+ <string name="upload_info_description">Descrizione:</string>
+ <string name="upload_info_private">Se deselezionata, è possibile visualizzare solo il filmato</string>
+ <string name="upload_info_tags">Tag:</string>
+ <string name="upload_info_title">Titolo:</string>
+ <string name="upload_info_upload">Carica</string>
+ <string name="uploadingNPhotos">Caricamento di <xliff:g id="counter">%d</xliff:g> immagini</string>
+ <string name="uploadingNVideos">Caricamento di <xliff:g id="counter">%d</xliff:g> video</string>
+ <string name="uploading_photos">Caricamento immagini in corso</string>
+ <string name="uploading_photos_2">Caricamento immagini in corso</string>
+ <string name="uploading_videos">Caricamento video in corso</string>
+ <string name="uploading_videos_2">Caricamento video in corso</string>
+ <string name="video_gallery_NoImageView_text">Nessun video trovato.</string>
+ <string name="video_play">Riproduci</string>
+ <string name="videos_gallery_title">Video</string>
+ <string name="view">Visualizza</string>
+ <string name="view_label">Visualizza immagine</string>
+ <string name="view_video_label">Visualizza video</string>
+ <string name="viewimage_comments_label_text">Commenti</string>
+ <string name="viewimage_datetaken_label_text">Data</string>
+ <string name="viewimage_digitalzoom_label_text">Zoom digitale</string>
+ <string name="viewimage_flash_label_text">Flash utilizzato</string>
+ <string name="viewimage_gps_latitude_label_text">Latitudine GPS</string>
+ <string name="viewimage_gps_longitude_label_text">Longitudine GPS</string>
+ <string name="viewimage_make_label_text">Crea</string>
+ <string name="viewimage_model_label_text">Da</string>
+ <string name="viewimage_orientation_label_text">Orientamento</string>
+ <string name="viewimage_res_label_text">Risoluzione</string>
+ <string name="viewimage_status_text">Premere Invio o Barra spaziatrice per eseguire lo zoom.</string>
+ <string name="wait">Attendere\u2026</string>
+ <string name="wallpaper">Impostazione dello sfondo in corso. Attendere\u2026</string>
+ <string name="yes">Sì</string>
+ <string name="youtube_upload_label">Caricamento YouTube</string>
+ <string name="zoom">Zoom</string>
+</resources>
diff --git a/res/values-nl-rNL/strings.xml b/res/values-nl-rNL/strings.xml
new file mode 100644
index 0000000..c2e7ce1
--- /dev/null
+++ b/res/values-nl-rNL/strings.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="autocrop_before_query">Vooraf automatisch inkorten?</string>
+ <string name="autocrop_no">Ja</string>
+ <string name="autocrop_yes">Ja</string>
+ <string name="best_res">Hoog</string>
+ <string name="camera_NoStorageView_text">Plaats een sd-kaart voordat u de camera gebruikt</string>
+ <string name="camera_ZoomIn_text">Inzoomen</string>
+ <string name="camera_ZoomOut_text">Uitzoomen</string>
+ <string name="camera_button_hint">Druk op de Vastlegtoets om een foto te maken</string>
+ <string name="camera_crop">Automatisch inkorten</string>
+ <string name="camera_done">Nieuwe foto</string>
+ <string name="camera_flash_auto">AutoFlits</string>
+ <string name="camera_flash_off">Flits UIT</string>
+ <string name="camera_flash_on">Flits AAN</string>
+ <string name="camera_flash_setting">Flitsinstelling</string>
+ <string name="camera_gallery_photos_text">Afbeeldingen</string>
+ <string name="camera_gallery_videos_text">Video's</string>
+ <string name="camera_keeping">Aanhoudend\u2026</string>
+ <string name="camera_label">Camera</string>
+ <string name="camera_mode_text">Modus</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_playvideo">Afspelen</string>
+ <string name="camera_record_text">Opname</string>
+ <string name="camera_selectphoto">Dit beeld selecteren</string>
+ <string name="camera_selectvideo">Deze video selecteren</string>
+ <string name="camera_set">Instellen als\u2026</string>
+ <string name="camera_setas_contact">Afbeelding contactpersoon </string>
+ <string name="camera_setas_myfave">myFaves</string>
+ <string name="camera_setas_wallpaper">Achtergrond</string>
+ <string name="camera_share">Delen</string>
+ <string name="camera_shareby_email">E-mail</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (alle foto's)</string>
+ <string name="camera_switch_to_photo">Schakelen naar foto</string>
+ <string name="camera_switch_to_video">Schakelen naar video</string>
+ <string name="camera_takenewphoto">Nieuwe afbeelding maken</string>
+ <string name="camera_takenewvideo">Nieuwe video maken</string>
+ <string name="camera_toss">Verwijderen</string>
+ <string name="camera_tossing">Bezig met verwijderen\u2026</string>
+ <string name="camera_zoom_1_text">Zoom 1.3x</string>
+ <string name="camera_zoom_2_text">Zoom 2x</string>
+ <string name="camera_zoom_3_text">Zoom 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">Instellingen</string>
+ <string name="camerasettings_autoupload_label">Afbeeldingen automatisch uploaden</string>
+ <string name="camerasettings_done">Gereed</string>
+ <string name="camerasettings_duration_label">Duur:</string>
+ <string name="camerasettings_image_quality_label">Beeldkwaliteit:</string>
+ <string name="camerasettings_no">Nee</string>
+ <string name="camerasettings_off">Uit</string>
+ <string name="camerasettings_on">Aan</string>
+ <string name="camerasettings_play_click_sound_label">Sluitergeluid</string>
+ <string name="camerasettings_preferred_view_label">Voorkeursweergave:</string>
+ <string name="camerasettings_resolution_label">Resolutie:</string>
+ <string name="camerasettings_test2_label">test2:</string>
+ <string name="camerasettings_test3_label">test3:</string>
+ <string name="camerasettings_test4_label">test4:</string>
+ <string name="camerasettings_use_sd_card_label">Afbeeldingen opslaan op sd-kaart</string>
+ <string name="camerasettings_yes">Ja</string>
+ <string name="cancel">Annuleren</string>
+ <string name="context_menu_header">Beeldopties</string>
+ <string name="crop_discard_text">Wissen</string>
+ <string name="crop_help">ALT ingedrukt houden om grootte te wijzigen</string>
+ <string name="crop_label">Afbeelding inkorten</string>
+ <string name="crop_save_text">Opslaan</string>
+ <string name="delete">Verwijderen</string>
+ <string name="details">Details</string>
+ <string name="details_category_label">Categorie:</string>
+ <string name="details_date_taken">Genomen op:</string>
+ <string name="details_description_label">Beschrijving:</string>
+ <string name="details_description_text">testbeschrijving</string>
+ <string name="details_file_size">Bestandsgrootte:</string>
+ <string name="details_image_resolution">Resolutie:</string>
+ <string name="details_language_label">Taal:</string>
+ <string name="details_panel_title">Details</string>
+ <string name="details_privateView_text">Privé</string>
+ <string name="details_publicView_text">Openbaar</string>
+ <string name="details_save_text">Opslaan</string>
+ <string name="details_tags_label">Labels:</string>
+ <string name="details_tags_text">monster</string>
+ <string name="details_title_label">Titel:</string>
+ <string name="details_title_text">mijnvideo</string>
+ <string name="details_uploaded_text">Uploaden gelukt</string>
+ <string name="edit">Bewerken</string>
+ <string name="error_label">Fout</string>
+ <string name="gallery_label">Foto's en video's</string>
+ <string name="gallery_large">Groot</string>
+ <string name="gallery_picker_label">Albums</string>
+ <string name="gallery_small">Klein</string>
+ <string name="gallerysettings_duration">Tijdsduur van elke dia in diashows</string>
+ <string name="gallerysettings_speed1">1 seconde</string>
+ <string name="gallerysettings_speed2">3 seconden</string>
+ <string name="gallerysettings_speed3">5 seconden</string>
+ <string name="high">Hoog</string>
+ <string name="image_gallery_NoImageView_text">Geen afbeeldingen</string>
+ <string name="image_gallery_picker_images">Afbeeldingen</string>
+ <string name="image_gallery_picker_videos">Video's</string>
+ <string name="low">Laag</string>
+ <string name="low_res">Laag</string>
+ <string name="med">Normaal</string>
+ <string name="medium_res">Normaal</string>
+ <string name="multiface_crop_help">Gezicht kiezen om te starten</string>
+ <string name="no">Nee</string>
+ <string name="no_images">Het album is leeg</string>
+ <string name="no_storage">Plaats een sd-kaart voordat u de camera gebruikt</string>
+ <string name="no_way_to_share_image">De afbeelding kan niet worden opgeslagen</string>
+ <string name="no_way_to_share_video">Deze video kan niet worden gedeeld</string>
+ <string name="not_enough_space">Deze geheugenkaart is vol</string>
+ <string name="ok">OK</string>
+ <string name="photos_gallery_title">Afbeeldingen</string>
+ <string name="picasa_upload_label">Uploaden naar Picasa</string>
+ <string name="pick_photos_gallery_title">Afbeelding selecteren</string>
+ <string name="pick_videos_gallery_title">Video selecteren</string>
+ <string name="pref_camera_autoupload_summary">Selecteren om afbeeldingen automatisch naar Picasa te uploaden</string>
+ <string name="pref_camera_autoupload_title">Afbeeldingen automatisch uploaden</string>
+ <string name="pref_camera_category">Camera</string>
+ <string name="pref_camera_recordlocation_summary">Selecteren om de locatie automatisch op te nemen bij foto's</string>
+ <string name="pref_camera_recordlocation_title">Locatie in afbeeldingen opnemen</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Picasa albumnaam</string>
+ <string name="pref_camera_upload_albumname_summary">Geef een naam aan het bestemmingsalbum voor afbeeldingen</string>
+ <string name="pref_camera_upload_albumname_title">Picasa albumnaam</string>
+ <string name="pref_gallery_category">Fotoalbumweergave</string>
+ <string name="pref_gallery_size_dialogtitle">Afbeeldinggrootte</string>
+ <string name="pref_gallery_size_summary">Selecteer de grootte van afbeeldingen in albums</string>
+ <string name="pref_gallery_size_title">Afbeeldinggrootte</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Diashowinterval</string>
+ <string name="pref_gallery_slideshow_interval_summary">Kies hoe lang elke dia wordt weergegeven</string>
+ <string name="pref_gallery_slideshow_interval_title">Diashowinterval</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Diashow herhalen?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Selecteren om de diashow meer dan eenmaal af te spelen</string>
+ <string name="pref_gallery_slideshow_repeat_title">Diashow herhalen</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Willekeurige volgorde?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Selecteren om afbeeldingen in willekeurige volgorde af te spelen</string>
+ <string name="pref_gallery_slideshow_shuffle_title">Willekeurige volgorde</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Diashowovergang</string>
+ <string name="pref_gallery_slideshow_transition_summary">Kies het effect als van de ene naar de andere dia wordt gewisseld</string>
+ <string name="pref_gallery_slideshow_transition_title">Diashowovergang</string>
+ <string name="pref_gallery_sort_dialogtitle">Afbeelding sorteren</string>
+ <string name="pref_gallery_sort_summary">Selecteer de volgorde van afbeeldingen in albums</string>
+ <string name="pref_gallery_sort_title">Afbeelding sorteren</string>
+ <string name="pref_slideshow_category">Diavoorstelling</string>
+ <string name="preferences_label">Camera-instellingen</string>
+ <string name="preview">Voorbld</string>
+ <string name="record">Opname</string>
+ <string name="rotate">Draaien</string>
+ <string name="rotate_left">Links draaien</string>
+ <string name="rotate_right">Rechts draaien</string>
+ <string name="runningFaceDetection">Een ogenblik geduld\u2026</string>
+ <string name="save">Opslaan</string>
+ <string name="savingImage">Afbeelding opslaan</string>
+ <string name="sec1">1 sec</string>
+ <string name="sec2">2 sec</string>
+ <string name="sec3">3 sec</string>
+ <string name="sec4">4 sec</string>
+ <string name="sec5">5 sec</string>
+ <string name="sendImage">Deze afbeelding delen via</string>
+ <string name="sendVideo">Deze video delen via</string>
+ <string name="set_wallpaper">Instellen als achtergrond</string>
+ <string name="settings">Instellingen</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">Diavoorstelling</string>
+ <string name="stop">Stoppen</string>
+ <string name="testexif">Exif testen</string>
+ <string name="upload">Uploaden</string>
+ <string name="upload_all">Alles uploaden</string>
+ <string name="upload_default_category_text">Entertainment</string>
+ <string name="upload_default_description_text">hier komt beschrijving</string>
+ <string name="upload_default_tags_text">video</string>
+ <string name="upload_dialog_title">Uploaden naar YouTube - voer videodetails in</string>
+ <string name="upload_info_category">Categorie:</string>
+ <string name="upload_info_description">Beschrijving:</string>
+ <string name="upload_info_private">Indien niet aangevinkt, zie je alleen de film</string>
+ <string name="upload_info_tags">Labels:</string>
+ <string name="upload_info_title">Titel:</string>
+ <string name="upload_info_upload">Uploaden</string>
+ <string name="uploadingNPhotos">Bezig met uploaden van <xliff:g id="counter">%d</xliff:g> afbeeldingen</string>
+ <string name="uploadingNVideos">Bezig met uploaden van <xliff:g id="counter">%d</xliff:g> video's</string>
+ <string name="uploading_photos">Afbeeldingen worden geüpload</string>
+ <string name="uploading_photos_2">Afbeeldingen worden geüpload</string>
+ <string name="uploading_videos">Video's worden geüpload</string>
+ <string name="uploading_videos_2">Video's worden geüpload</string>
+ <string name="video_gallery_NoImageView_text">Geen video's</string>
+ <string name="video_play">Afspelen</string>
+ <string name="videos_gallery_title">Video's</string>
+ <string name="view_label">Afbeelding weergeven</string>
+ <string name="view_video_label">Video weergeven</string>
+ <string name="viewimage_comments_label_text">Opmerkingen</string>
+ <string name="viewimage_datetaken_label_text">Datum</string>
+ <string name="viewimage_digitalzoom_label_text">Digitale zoom</string>
+ <string name="viewimage_flash_label_text">Flits gebruikt</string>
+ <string name="viewimage_gps_latitude_label_text">Gps-breedtegraad</string>
+ <string name="viewimage_gps_longitude_label_text">Gps-lengtegraad</string>
+ <string name="viewimage_make_label_text">Maken</string>
+ <string name="viewimage_model_label_text">Van</string>
+ <string name="viewimage_orientation_label_text">Afdrukstand</string>
+ <string name="viewimage_res_label_text">Resolutie</string>
+ <string name="viewimage_status_text">Druk op Enter of Spatie om in te zoomen</string>
+ <string name="wait">Een ogenblik geduld\u2026</string>
+ <string name="wallpaper">Achtergrond wordt ingesteld, een ogenblik geduld\u2026</string>
+ <string name="yes">Ja</string>
+ <string name="youtube_upload_label">Uploaden naar YouTube</string>
+ <string name="zoom">In-/uitzoomen</string>
+</resources>
diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..32c1e51
--- /dev/null
+++ b/res/values-zh-rTW/strings.xml
@@ -0,0 +1,229 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="all_images">所有圖片</string>
+ <string name="autocrop_before_query">先裁剪?</string>
+ <string name="autocrop_no">是</string>
+ <string name="autocrop_yes">是</string>
+ <string name="best_res">高</string>
+ <string name="camera_NoStorageView_text">請先插入 SD 卡再使用相機。</string>
+ <string name="camera_ZoomIn_text">拉近</string>
+ <string name="camera_ZoomOut_text">拉遠</string>
+ <string name="camera_button_hint">按下拍攝鍵即可拍攝圖片。</string>
+ <string name="camera_crop">裁剪</string>
+ <string name="camera_done">新增圖片</string>
+ <string name="camera_flash_auto">閃光燈自動</string>
+ <string name="camera_flash_off">閃光燈關閉</string>
+ <string name="camera_flash_on">閃光燈開啟</string>
+ <string name="camera_flash_setting">閃光燈設定</string>
+ <string name="camera_gallery_photos_text">圖片</string>
+ <string name="camera_gallery_videos_text">影片輯</string>
+ <string name="camera_keep">儲存</string>
+ <string name="camera_keeping">正在保留\u2026</string>
+ <string name="camera_label">相機</string>
+ <string name="camera_mode_text">模式</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_pick_wallpaper">圖片</string>
+ <string name="camera_playvideo">播放</string>
+ <string name="camera_record_text">錄音</string>
+ <string name="camera_selectphoto">選取此圖片</string>
+ <string name="camera_selectvideo">選取此影片</string>
+ <string name="camera_set">設成</string>
+ <string name="camera_setas_contact">連絡人圖片</string>
+ <string name="camera_setas_myfave">我的最愛</string>
+ <string name="camera_setas_wallpaper">桌布</string>
+ <string name="camera_setas_wallpaper_drm">購買的圖片</string>
+ <string name="camera_share">共用</string>
+ <string name="camera_shareby_email">電子郵件</string>
+ <string name="camera_shareby_mms">多媒體簡訊</string>
+ <string name="camera_shareby_picasa">Picasa 網路相簿</string>
+ <string name="camera_shareby_picasa_all">Picasa 網路相簿 (所有圖片)</string>
+ <string name="camera_switch_to_photo">切換到圖片</string>
+ <string name="camera_switch_to_video">切換到影片</string>
+ <string name="camera_takenewphoto">拍攝新圖片</string>
+ <string name="camera_takenewvideo">拍攝新影片</string>
+ <string name="camera_toss">刪除</string>
+ <string name="camera_tossing">正在刪除\u2026</string>
+ <string name="camera_zoom_1_text">顯示比例 1.3x</string>
+ <string name="camera_zoom_2_text">顯示比例 2x</string>
+ <string name="camera_zoom_3_text">顯示比例 4x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+ <string name="camerasettings">設定</string>
+ <string name="camerasettings_autoupload_label">自動將圖片上傳到網頁</string>
+ <string name="camerasettings_done">完成</string>
+ <string name="camerasettings_duration_label">持續期間:</string>
+ <string name="camerasettings_image_quality_label">圖片品質:</string>
+ <string name="camerasettings_no">否</string>
+ <string name="camerasettings_off">關閉</string>
+ <string name="camerasettings_on">開啟</string>
+ <string name="camerasettings_play_click_sound_label">快門聲</string>
+ <string name="camerasettings_preferred_view_label">偏好的檢視:</string>
+ <string name="camerasettings_resolution_label">解析度:</string>
+ <string name="camerasettings_test2_label">測試 2:</string>
+ <string name="camerasettings_test3_label">測試 3:</string>
+ <string name="camerasettings_test4_label">測試 4:</string>
+ <string name="camerasettings_use_sd_card_label">將圖片儲存到 SD 卡</string>
+ <string name="camerasettings_yes">是</string>
+ <string name="cancel">取消</string>
+ <string name="confirm_delete">確認刪除</string>
+ <string name="confirm_delete_message">將會刪除此圖片。</string>
+ <string name="confirm_delete_title">刪除</string>
+ <string name="context_menu_header">圖片選項</string>
+ <string name="crop_discard_text">放棄</string>
+ <string name="crop_help">按住 ALT 調整大小。</string>
+ <string name="crop_label">裁剪圖片</string>
+ <string name="crop_save_text">儲存</string>
+ <string name="default_value_pref_gallery_size">1</string>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+ <string name="default_value_pref_gallery_sort">遞減</string>
+ <string name="delete">刪除</string>
+ <string name="deny_delete">取消"</string>
+ <string name="details">詳細資料</string>
+ <string name="details_category_label">類別:</string>
+ <string name="details_date_taken">拍攝日期:</string>
+ <string name="details_description_label">說明:</string>
+ <string name="details_description_text">測試說明</string>
+ <string name="details_file_size">檔案大小:</string>
+ <string name="details_image_resolution">解析度:</string>
+ <string name="details_language_label">語言:</string>
+ <string name="details_panel_title">詳細資料</string>
+ <string name="details_privateView_text">私人</string>
+ <string name="details_publicView_text">公開</string>
+ <string name="details_save_text">儲存</string>
+ <string name="details_tags_label">標籤:</string>
+ <string name="details_tags_text">monster</string>
+ <string name="details_title_label">標題:</string>
+ <string name="details_title_text">我的影片</string>
+ <string name="details_uploaded_text">上傳成功</string>
+ <string name="edit">編輯</string>
+ <string name="flip_orientation">翻轉方向</string>
+ <string name="gallery_label">圖片</string>
+ <string name="gallery_large">大</string>
+ <string name="gallery_picker_label">圖片</string>
+ <string name="gallery_small">小</string>
+ <string name="gallerysettings_duration">投影片放映中每張投影片顯示的持續時間</string>
+ <string name="gallerysettings_speed1">1 秒</string>
+ <string name="gallerysettings_speed2">3 秒</string>
+ <string name="gallerysettings_speed3">5 秒</string>
+ <string name="high">高</string>
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> 張圖片</string>
+ <string name="image_gallery_NoImageView_text">找不到圖片。</string>
+ <string name="image_gallery_picker_images">圖片</string>
+ <string name="image_gallery_picker_videos">影片</string>
+ <string name="loading_progress_format_string">剩餘 <xliff:g id="counter">%d</xliff:g></string>
+ <string name="low">低</string>
+ <string name="low_res">低</string>
+ <string name="med">中</string>
+ <string name="medium_res">中</string>
+ <string name="multiface_crop_help">點選臉部開始。</string>
+ <string name="no">否</string>
+ <string name="no_images">空白的相簿。</string>
+ <string name="no_storage">請先插入 SD 卡再使用相機。</string>
+ <string name="no_way_to_share_image">無法儲存此圖片。</string>
+ <string name="no_way_to_share_video">無法共用此影片。</string>
+ <string name="not_enough_space">SD 卡已滿。</string>
+ <string name="ok">確定</string>
+ <string name="photos_gallery_title">圖片</string>
+ <string name="picasa_upload_label">上傳到 Picasa</string>
+ <string name="pick_photos_gallery_title">選取圖片</string>
+ <string name="pick_videos_gallery_title">選取影片</string>
+ <string name="pref_camera_autoupload_summary">自動將圖片上傳到 Picasa</string>
+ <string name="pref_camera_autoupload_title">自動上傳圖片</string>
+ <string name="pref_camera_category">相機</string>
+ <string name="pref_camera_postpicturemenu_summary">拍攝之後顯示動作功能表 (儲存、刪除...)</string>
+ <string name="pref_camera_postpicturemenu_title">拍攝之後提示</string>
+ <string name="pref_camera_recordlocation_summary">在圖片資料中記錄位置</string>
+ <string name="pref_camera_recordlocation_title">將位置儲存在圖片中</string>
+ <string name="pref_camera_shuttersound_summary">拍攝圖片時播放聲音</string>
+ <string name="pref_camera_shuttersound_title">播放快門聲</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Picasa 相簿名稱</string>
+ <string name="pref_camera_upload_albumname_summary">命名圖片的目的相簿 (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_title">Picasa 相簿名稱</string>
+ <string name="pref_gallery_category">一般設定</string>
+ <string name="pref_gallery_confirm_delete_summary">刪除圖片之前顯示確認</string>
+ <string name="pref_gallery_confirm_delete_title">確認刪除</string>
+ <string name="pref_gallery_size_dialogtitle">圖片大小</string>
+ <string name="pref_gallery_size_summary">選取圖片的顯示大小</string>
+ <string name="pref_gallery_size_title">圖片大小</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">投影片放映間隔</string>
+ <string name="pref_gallery_slideshow_interval_summary">選取投影片放映中每張投影片顯示的時間</string>
+ <string name="pref_gallery_slideshow_interval_title">投影片放映間隔</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">重複投影片放映?</string>
+ <string name="pref_gallery_slideshow_repeat_summary">投影片放映次數超過一次</string>
+ <string name="pref_gallery_slideshow_repeat_title">重複投影片放映</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">隨機播放圖片?</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">以隨機順序顯示圖片</string>
+ <string name="pref_gallery_slideshow_shuffle_title">隨機播放投影片</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">投影片放映轉換</string>
+ <string name="pref_gallery_slideshow_transition_summary">選取從一張投影片移動到下一張投影片時所使用的特效</string>
+ <string name="pref_gallery_slideshow_transition_title">投影片放映轉換</string>
+ <string name="pref_gallery_sort_dialogtitle">圖片排序</string>
+ <string name="pref_gallery_sort_summary">選取圖片的排序方式</string>
+ <string name="pref_gallery_sort_title">圖片排序</string>
+ <string name="pref_slideshow_category">投影片放映設定</string>
+ <string name="preferences_label">相機設定</string>
+ <string name="preview">預覽</string>
+ <string name="record">錄音</string>
+ <string name="rotate">旋轉</string>
+ <string name="rotate_left">向左旋轉</string>
+ <string name="rotate_right">向右旋轉</string>
+ <string name="runningFaceDetection">請稍候\u2026</string>
+ <string name="save">儲存</string>
+ <string name="savingImage">正在儲存圖片\u2026</string>
+ <string name="sec1">1 秒</string>
+ <string name="sec2">2 秒</string>
+ <string name="sec3">3 秒</string>
+ <string name="sec4">4 秒</string>
+ <string name="sec5">5 秒</string>
+ <string name="sendImage">共用圖片</string>
+ <string name="sendVideo">共用影片</string>
+ <string name="setImage">將圖片設成</string>
+ <string name="set_wallpaper">設成桌布</string>
+ <string name="settings">設定</string>
+ <string name="share_youtube">YouTube</string>
+ <string name="slide_show">投影片放映</string>
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+ <string name="stop">停止</string>
+ <string name="testexif">測試 Exif</string>
+ <string name="upload">上傳</string>
+ <string name="upload_all">全部上傳</string>
+ <string name="upload_default_category_text">娛樂</string>
+ <string name="upload_default_description_text">在此輸入說明</string>
+ <string name="upload_default_tags_text">影片</string>
+ <string name="upload_dialog_title">上傳到 YouTube - 輸入影片詳細資料</string>
+ <string name="upload_info_category">類別:</string>
+ <string name="upload_info_description">說明:</string>
+ <string name="upload_info_private">如果取消勾選此選項,則只有您可以觀看此電影</string>
+ <string name="upload_info_tags">標籤:</string>
+ <string name="upload_info_title">標題:</string>
+ <string name="upload_info_upload">上傳</string>
+ <string name="uploadingNPhotos">正在上傳 <xliff:g id="counter">%d</xliff:g> 張圖片</string>
+ <string name="uploadingNVideos">正在上傳 <xliff:g id="counter">%d</xliff:g> 段影片</string>
+ <string name="uploading_photos">正在上傳圖片</string>
+ <string name="uploading_photos_2">正在上傳圖片</string>
+ <string name="uploading_videos">正在上傳影片</string>
+ <string name="uploading_videos_2">正在上傳影片</string>
+ <string name="video_gallery_NoImageView_text">找不到影片。</string>
+ <string name="video_play">播放</string>
+ <string name="videos_gallery_title">影片</string>
+ <string name="view">檢視</string>
+ <string name="view_label">檢視圖片</string>
+ <string name="view_video_label">檢視影片</string>
+ <string name="viewimage_comments_label_text">註解</string>
+ <string name="viewimage_datetaken_label_text">日期</string>
+ <string name="viewimage_digitalzoom_label_text">數位顯示比例</string>
+ <string name="viewimage_flash_label_text">使用 Flash</string>
+ <string name="viewimage_gps_latitude_label_text">GPS 緯度</string>
+ <string name="viewimage_gps_longitude_label_text">GPS 經度</string>
+ <string name="viewimage_make_label_text">製造</string>
+ <string name="viewimage_model_label_text">從</string>
+ <string name="viewimage_orientation_label_text">方向</string>
+ <string name="viewimage_res_label_text">解析度</string>
+ <string name="viewimage_status_text">按下 Enter 或 Space 來顯示比例。</string>
+ <string name="wait">請稍候\u2026</string>
+ <string name="wallpaper">正在設定桌布,請稍候\u2026</string>
+ <string name="yes">是</string>
+ <string name="youtube_upload_label">YouTube 上傳</string>
+ <string name="zoom">顯示比例</string>
+</resources>
diff --git a/res/values/ids.xml b/res/values/ids.xml
new file mode 100644
index 0000000..d60f413
--- /dev/null
+++ b/res/values/ids.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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>
+ <item type="id" name="autoupload" />
+ <item type="id" name="duration" />
+ <item type="id" name="imagequality" />
+ <item type="id" name="playclicksound" />
+ <item type="id" name="preferred_gallery_view" />
+ <item type="id" name="resolution" />
+ <item type="id" name="t2" />
+ <item type="id" name="t3" />
+ <item type="id" name="t4" />
+ <item type="id" name="usesd" />
+ <item type="id" name="slideshowdelay" />
+</resources>
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100755
index 0000000..e5b47fb
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,368 @@
+<?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 -->
+
+ <string name="all_images">All pictures</string>
+
+ <string name="image_count"><xliff:g id="counter">%d</xliff:g> pictures</string>
+ <string name="camera_label">Camera</string>
+ <string name="gallery_picker_label">Pictures</string>
+ <string name="gallery_label">Pictures</string>
+ <string name="crop_label">Crop picture</string>
+ <string name="view_label">View picture</string>
+ <string name="view_video_label">View video</string>
+ <string name="preferences_label">Camera settings</string>
+ <string name="picasa_upload_label">Picasa upload</string>
+ <string name="youtube_upload_label">YouTube upload</string>
+
+ <string name="ok">OK</string>
+ <string name="stop">Stop</string>
+ <string name="record">Record</string>
+ <string name="wait">Please wait\u2026</string>
+ <string name="no_storage">Please insert an SD card before using the camera.</string>
+ <string name="not_enough_space">Your SD card is full.</string>
+ <string name="no_images">Your album is empty.</string>
+ <string name="wallpaper">Setting wallpaper, please wait\u2026</string>
+ <string name="gallery_small">Small</string>
+ <string name="gallery_large">Large</string>
+
+ <!-- Settings stuff -->
+ <string name="high">High</string>
+ <string name="med">Medium</string>
+ <string name="low">Low</string>
+
+ <string name="best_res">High</string>
+ <string name="medium_res">Medium</string>
+ <string name="low_res">Low</string>
+
+ <string name="sec1">1 sec</string>
+ <string name="sec2">2 secs</string>
+ <string name="sec3">3 secs</string>
+ <string name="sec4">4 secs</string>
+ <string name="sec5">5 secs</string>
+
+ <string name="yes">Yes</string>
+ <string name="no">No</string>
+
+ <string name="savingImage">Saving picture\u2026</string>
+ <string name="runningFaceDetection">Please wait\u2026</string>
+
+
+ <!-- Menu stuff -->
+ <string name="flip_orientation">Flip orientation</string>
+ <string name="settings">Settings</string>
+ <string name="zoom">Zoom</string>
+ <string name="testexif">Test Exif</string>
+
+ <string name="view">View</string>
+ <string name="delete">Delete</string>
+ <string name="details">Details</string>
+ <string name="rotate">Rotate</string>
+ <string name="rotate_left">Rotate left</string>
+ <string name="rotate_right">Rotate right</string>
+ <string name="set_wallpaper">Set as wallpaper</string>
+ <string name="slide_show">Slideshow</string>
+
+ <string name="confirm_delete">Confirm delete</string>
+ <string name="deny_delete">Cancel"</string>
+
+ <string name="upload">Upload</string>
+ <string name="upload_all">Upload all</string>
+ <string name="uploading_photos">Uploading pictures</string>
+ <string name="uploading_photos_2">Uploading pictures</string>
+ <string name="uploading_videos">Uploading videos</string>
+ <string name="uploading_videos_2">Uploading videos</string>
+ <string name="uploadingNPhotos">Uploading <xliff:g id="counter">%d</xliff:g> pictures</string>
+ <string name="uploadingNVideos">Uploading <xliff:g id="counter">%d</xliff:g> videos</string>
+
+ <string name="preview">Preview</string>
+ <string name="edit">Edit</string>
+ <string name="cancel">Cancel</string>
+ <string name="save">Save</string>
+
+ <string name="crop_help">Hold ALT to resize.</string>
+ <string name="crop_save_text">Save</string>
+ <string name="crop_discard_text">Discard</string>
+
+ <string name="camera_mode_text">Mode</string>
+ <string name="camera_record_text">Record</string>
+
+ <string name="camera_keep">Save</string>
+ <string name="confirm_delete_title">Delete</string>
+ <string name="confirm_delete_message">The picture will be deleted.</string>
+
+ <string name="camera_toss">Delete</string>
+ <string name="camera_share">Share</string>
+ <string name="camera_set">Set as</string>
+ <string name="camera_crop">Crop</string>
+ <string name="camera_done">New picture</string>
+
+ <string name="camera_tossing">Deleting\u2026</string>
+ <string name="camera_keeping">Keeping\u2026</string>
+ <string name="camera_flash_setting">Flash setting</string>
+
+ <string name="no_way_to_share_image">This picture cannot be saved.</string>
+ <string name="no_way_to_share_video">This video cannot be shared.</string>
+
+ <string name="autocrop_before_query">Crop before?</string>
+ <string name="autocrop_yes">Yes</string>
+ <string name="autocrop_no">Yes</string>
+ <string name="video_play">Play</string>
+ <string name="share_youtube">YouTube</string>
+
+ <string name="camera_switch_to_video">Switch to video</string>
+ <string name="camera_switch_to_photo">Switch to picture</string>
+
+ <string name="camera_NoStorageView_text">Please insert an SD card before using the camera.</string>
+ <string name="camera_ZoomIn_text">Zoom in</string>
+ <string name="camera_ZoomOut_text">Zoom out</string>
+ <string name="camera_movie_record_counter_text">01:30:00</string>
+ <string name="camera_gallery_photos_text">Pictures</string>
+ <string name="camera_gallery_videos_text">Video Album</string>
+
+ <string name="camera_shareby_email">Email</string>
+ <string name="camera_shareby_mms">MMS</string>
+ <string name="camera_shareby_picasa">Picasaweb</string>
+ <string name="camera_shareby_picasa_all">Picasaweb (all pictures)</string>
+
+ <string name="camera_flash_auto">AutoFlash</string>
+ <string name="camera_flash_on">Flash ON</string>
+ <string name="camera_flash_off">Flash OFF</string>
+
+ <string name="camera_pick_wallpaper">Pictures</string>
+ <string name="camera_setas_wallpaper">Wallpaper</string>
+
+ <string name="camera_setas_wallpaper_drm">Purchased pictures</string>
+ <string name="camera_setas_contact">Contact picture</string>
+ <string name="camera_setas_myfave">myFaves</string>
+
+ <string name="camera_selectphoto">Select this picture</string>
+ <string name="camera_takenewphoto">Capture new picture</string>
+
+ <string name="camera_selectvideo">Select this video</string>
+ <string name="camera_takenewvideo">Capture new video</string>
+ <string name="camera_playvideo">Play</string>
+
+ <string name="pref_gallery_category">General settings</string>
+ <string name="pref_slideshow_category">Slideshow settings</string>
+ <string name="pref_camera_category">Camera</string>
+
+ <string name="pref_gallery_size_title">Picture size</string>
+ <string name="pref_gallery_size_summary">Select the display size of pictures</string>
+ <string name="pref_gallery_size_dialogtitle">Picture size</string>
+ <string-array name="pref_gallery_size_choices">
+ <item>Large</item>
+ <item>Small</item>
+ </string-array>
+
+ <string-array name="pref_gallery_size_values">
+ <item>1</item>
+ <item>0</item>
+ </string-array>
+ <string name="default_value_pref_gallery_size">1</string>
+
+ <string name="pref_gallery_sort_title">Picture sort</string>
+ <string name="pref_gallery_sort_summary">Select the sort order of pictures</string>
+ <string name="pref_gallery_sort_dialogtitle">Picture sort</string>
+ <string-array name="pref_gallery_sort_choices">
+ <item>Newest first</item>
+ <item>Newest last</item>
+ </string-array>
+
+ <string-array name="pref_gallery_sort_values">
+ <item>descending</item>
+ <item>ascending</item>
+ </string-array>
+ <string name="default_value_pref_gallery_sort">descending</string>
+
+ <string name="pref_gallery_slideshow_interval_title">Slideshow interval</string>
+ <string name="pref_gallery_slideshow_interval_summary">Select how long each slide displays in the show</string>
+ <string name="pref_gallery_slideshow_interval_dialogtitle">Slideshow interval</string>
+ <string-array name="pref_gallery_slideshow_interval_choices">
+ <item>2 seconds</item>
+ <item>3 seconds</item>
+ <item>4 seconds</item>
+ </string-array>
+
+ <string-array name="pref_gallery_slideshow_interval_values">
+ <item>"2"</item>
+ <item>"3"</item>
+ <item>"4"</item>
+ </string-array>
+ <string name="default_value_pref_gallery_slideshow_interval">"2"</string>
+
+ <string name="pref_gallery_slideshow_transition_title">Slideshow transition</string>
+ <string name="pref_gallery_slideshow_transition_summary">Select the effect used when moving from one slide to the next</string>
+ <string name="pref_gallery_slideshow_transition_dialogtitle">Slideshow transition</string>
+ <string-array name="pref_gallery_slideshow_transition_choices">
+ <item>Fade in &amp; out</item>
+ <item>Slide left - right</item>
+ <item>Slide up - down</item>
+ <item>Random selection</item>
+ </string-array>
+
+ <string-array name="pref_gallery_slideshow_transition_values">
+ <item>"0"</item>
+ <item>"1"</item>
+ <item>"2"</item>
+ <item>"-1"</item>
+ </string-array>
+ <string name="default_value_pref_gallery_slideshow_transition">"0"</string>
+
+ <string name="pref_gallery_slideshow_repeat_title">Repeat slideshow</string>
+ <string name="pref_gallery_slideshow_repeat_summary">Play slideshow more than once</string>
+ <string name="pref_gallery_slideshow_repeat_dialogtitle">Repeat slideshow?</string>
+
+ <string name="pref_gallery_slideshow_shuffle_title">Shuffle slides</string>
+ <string name="pref_gallery_slideshow_shuffle_summary">Show pictures in random order</string>
+ <string name="pref_gallery_slideshow_shuffle_dialogtitle">Shuffle pictures?</string>
+
+ <string name="pref_camera_autoupload_title">Auto-upload pictures</string>
+ <string name="pref_camera_autoupload_summary">Upload pictures to Picasa automatically</string>
+
+ <string name="pref_camera_recordlocation_title">Store location in pictures</string>
+ <string name="pref_camera_recordlocation_summary">Record location in picture data</string>
+
+ <string name="pref_camera_shuttersound_title">Play shutter sound</string>
+ <string name="pref_camera_shuttersound_summary">Play a sound when taking a picture</string>
+
+ <string name="pref_camera_upload_albumname_title">Picasa album name</string>
+ <string name="pref_camera_upload_albumname_summary">Name the destination album for your pictures (<xliff:g id="summary">%s</xliff:g>)</string>
+ <string name="pref_camera_upload_albumname_dialogtitle">Picasa album name</string>
+
+ <string name="pref_camera_postpicturemenu_title">Prompt after capture</string>
+ <string name="pref_camera_postpicturemenu_summary">Display action menu (save, delete, ...) after capture</string>
+
+ <string name="camerasettings_preferred_view_label">Preferred view:</string>
+ <string name="camerasettings">Settings</string>
+ <string name="camerasettings_autoupload_label">Automatically upload pictures to Web</string>
+ <string name="camerasettings_test2_label">test2:</string>
+ <string name="camerasettings_test3_label">test3:</string>
+ <string name="camerasettings_play_click_sound_label">Shutter sound</string>
+ <string name="camerasettings_resolution_label">Resolution:</string>
+ <string name="camerasettings_test4_label">test4:</string>
+ <string name="camerasettings_duration_label">Duration:</string>
+ <string name="camerasettings_use_sd_card_label">Save pictures to SD card</string>
+ <string name="camerasettings_image_quality_label">Picture quality:</string>
+ <string name="camerasettings_on">On</string>
+ <string name="camerasettings_off">Off</string>
+ <string name="camerasettings_yes">Yes</string>
+ <string name="camerasettings_no">No</string>
+ <string name="camerasettings_done">Done</string>
+
+ <string name="gallerysettings_duration">Duration of each slide in slideshows</string>
+
+ <!-- The actual seconds are hardwired in GallerySettings.java. These are just labels. -->
+ <string name="gallerysettings_speed1">1 second</string>
+ <string name="gallerysettings_speed2">3 seconds</string>
+ <string name="gallerysettings_speed3">5 seconds</string>
+
+ <string name="image_gallery_status_text"></string>
+ <string name="image_gallery_NoImageView_text">No pictures found.</string>
+ <string name="video_gallery_NoImageView_text">No videos found.</string>
+
+ <string name="image_gallery_picker_images">Pictures</string>
+ <string name="image_gallery_picker_videos">Videos</string>
+
+ <string name="pref_gallery_confirm_delete_title">Confirm deletions</string>
+ <string name="pref_gallery_confirm_delete_summary">Show confirmation before deleting pictures</string>
+
+ <string name="viewimage_gps_latitude_label_text">GPS latitude</string>
+ <string name="viewimage_res_label_text">Resolution</string>
+ <string name="viewimage_make_label_text">Make</string>
+ <string name="viewimage_comments_text"></string>
+ <string name="viewimage_digitalzoom_text"></string>
+ <string name="viewimage_orientation_label_text">Orientation</string>
+ <string name="viewimage_res_text"></string>
+ <string name="viewimage_gps_longitude_label_text">GPS longitude</string>
+ <string name="viewimage_model_text"></string>
+ <string name="viewimage_gps_longitude_text"></string>
+ <string name="viewimage_make_text"></string>
+ <string name="viewimage_status_text">Press Enter or Space to zoom.</string>
+ <string name="viewimage_comments_label_text">Comments</string>
+ <string name="viewimage_flash_text"></string>
+ <string name="viewimage_digitalzoom_label_text">Digital zoom</string>
+ <string name="viewimage_orientation_text"></string>
+ <string name="viewimage_flash_label_text">Flash used</string>
+ <string name="viewimage_gps_latitude_text"></string>
+ <string name="viewimage_datetaken_text"></string>
+ <string name="viewimage_model_label_text">From</string>
+ <string name="viewimage_datetaken_label_text">Date</string>
+
+ <string name="camera_zoom_3_text">Zoom 4x</string>
+ <string name="camera_zoom_2_text">Zoom 2x</string>
+ <string name="camera_zoom_1_text">Zoom 1.3x</string>
+ <string name="camera_zoom_normal_text">1x</string>
+
+ <!-- Details stuff -->
+ <string name="details_title_text">myvideo</string>
+ <string name="details_uploaded_text">Successfully uploaded</string>
+ <string name="details_tags_text">monster</string>
+ <string name="details_category_label">Category:</string>
+ <string name="details_description_label">Description:</string>
+ <string name="details_publicView_text">Public</string>
+ <string name="details_tags_label">Tags:</string>
+ <string name="details_description_text">testdescription</string>
+ <string name="details_title_label">Title:</string>
+ <string name="details_save_text">Save</string>
+ <string name="details_language_label">Language:</string>
+ <string name="details_privateView_text">Private</string>
+
+ <!-- Upload stuff -->
+ <string name="upload_default_tags_text">video</string>
+ <string name="upload_default_description_text">description goes here</string>
+ <string name="upload_default_category_text">Entertainment</string>
+
+ <string name="details_panel_title">Details</string>
+
+ <string name="details_file_size">File size:</string>
+ <string name="details_image_resolution">Resolution:</string>
+ <string name="details_date_taken">Date taken:</string>
+
+ <string name="context_menu_header">Picture options</string>
+
+ <string name="multiface_crop_help">Tap a face to begin.</string>
+ <string name="camera_button_hint">Press Capture button to take picture.</string>
+
+ <string name="photos_gallery_title">Pictures</string>
+ <string name="videos_gallery_title">Videos</string>
+ <string name="pick_photos_gallery_title">Select picture</string>
+ <string name="pick_videos_gallery_title">Select video</string>
+
+ <!-- YouTube upload stuff -->
+ <string name="upload_info_title">Title:</string>
+ <string name="upload_info_description">Description:</string>
+ <string name="upload_info_category">Category:</string>
+ <string name="upload_info_tags">Tags:</string>
+ <string name="upload_info_upload">Upload</string>
+ <string name="upload_info_private">If unchecked, only you can see the movie</string>
+ <string name="upload_dialog_title">Upload to YouTube - type video details</string>
+
+ <string name="loading_progress_format_string"><xliff:g id="counter">%d</xliff:g> remaining</string>
+
+ <string name="space_remaining_k"><xliff:g id="counter">%d</xliff:g>K</string>
+
+ <!-- Displayed in the title of the chooser for things to do with an image that
+ is to be sent to another application. -->
+ <string name="sendImage">Share picture via</string>
+ <string name="setImage">Set picture as</string>
+
+ <!-- Displayed in the title of the chooser for things to do with a video that
+ is to be sent to another application. -->
+ <string name="sendVideo">Share video via</string>
+</resources>
diff --git a/res/xml/camera_preferences.xml b/res/xml/camera_preferences.xml
new file mode 100644
index 0000000..c4dab52
--- /dev/null
+++ b/res/xml/camera_preferences.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<!-- This is a primitive example showing the different types of preferences available. -->
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <PreferenceCategory
+ android:title="@string/pref_camera_category">
+<!--
+ <CheckBoxPreference
+ android:key="pref_camera_autoupload_key"
+ android:title="@string/pref_camera_autoupload_title"
+ android:summary="@string/pref_camera_autoupload_summary"
+ android:defaultValue="false"/>
+ <EditTextPreference
+ android:key="pref_camera_upload_albumname_key"
+ android:title="@string/pref_camera_upload_albumname_title"
+ android:summary="@string/pref_camera_upload_albumname_summary"
+ android:dialogTitle="@string/pref_camera_upload_albumname_dialogtitle"
+ android:singleLine="true" />
+ <CheckBoxPreference
+ android:key="pref_camera_shuttersound_key"
+ android:title="@string/pref_camera_shuttersound_title"
+ android:summary="@string/pref_camera_shuttersound_summary"
+ android:defaultValue="true"/>
+-->
+ <CheckBoxPreference
+ android:key="pref_camera_recordlocation_key"
+ android:title="@string/pref_camera_recordlocation_title"
+ android:summary="@string/pref_camera_recordlocation_summary"
+ android:defaultValue="false"/>
+ <CheckBoxPreference
+ android:key="pref_camera_postpicturemenu_key"
+ android:title="@string/pref_camera_postpicturemenu_title"
+ android:summary="@string/pref_camera_postpicturemenu_summary"
+ android:defaultValue="true"/>
+ </PreferenceCategory>
+
+</PreferenceScreen>
diff --git a/res/xml/gallery_preferences.xml b/res/xml/gallery_preferences.xml
new file mode 100644
index 0000000..0ddc166
--- /dev/null
+++ b/res/xml/gallery_preferences.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<!-- This is a primitive example showing the different types of preferences available. -->
+<PreferenceScreen
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <PreferenceCategory
+ android:title="@string/pref_gallery_category">
+
+ <ListPreference
+ android:key="pref_gallery_size_key"
+ android:title="@string/pref_gallery_size_title"
+ android:summary="@string/pref_gallery_size_summary"
+ android:entries="@array/pref_gallery_size_choices"
+ android:entryValues="@array/pref_gallery_size_values"
+ android:dialogTitle="@string/pref_gallery_size_dialogtitle"
+ android:defaultValue="@string/default_value_pref_gallery_size" />
+
+ <ListPreference
+ android:key="pref_gallery_sort_key"
+ android:title="@string/pref_gallery_sort_title"
+ android:summary="@string/pref_gallery_sort_summary"
+ android:entries="@array/pref_gallery_sort_choices"
+ android:entryValues="@array/pref_gallery_sort_values"
+ android:dialogTitle="@string/pref_gallery_sort_dialogtitle"
+ android:defaultValue="@string/default_value_pref_gallery_sort" />
+
+ <CheckBoxPreference
+ android:key="pref_gallery_confirm_delete_key"
+ android:title="@string/pref_gallery_confirm_delete_title"
+ android:summary="@string/pref_gallery_confirm_delete_summary"
+ android:defaultValue="true"/>
+<!--
+ <EditTextPreference
+ android:key="pref_camera_upload_albumname_key"
+ android:title="@string/pref_camera_upload_albumname_title"
+ android:summary="@string/pref_camera_upload_albumname_summary"
+ android:dialogTitle="@string/pref_camera_upload_albumname_dialogtitle"
+ android:singleLine="true" />
+-->
+ </PreferenceCategory>
+ <PreferenceCategory
+ android:title="@string/pref_slideshow_category">
+ <ListPreference
+ android:key="pref_gallery_slideshow_interval_key"
+ android:title="@string/pref_gallery_slideshow_interval_title"
+ android:summary="@string/pref_gallery_slideshow_interval_summary"
+ android:entries="@array/pref_gallery_slideshow_interval_choices"
+ android:entryValues="@array/pref_gallery_slideshow_interval_values"
+ android:dialogTitle="@string/pref_gallery_slideshow_interval_dialogtitle"
+ android:defaultValue="@string/default_value_pref_gallery_slideshow_interval" />
+
+ <ListPreference
+ android:key="pref_gallery_slideshow_transition_key"
+ android:title="@string/pref_gallery_slideshow_transition_title"
+ android:summary="@string/pref_gallery_slideshow_transition_summary"
+ android:entries="@array/pref_gallery_slideshow_transition_choices"
+ android:entryValues="@array/pref_gallery_slideshow_transition_values"
+ android:dialogTitle="@string/pref_gallery_slideshow_transition_dialogtitle"
+ android:defaultValue="@string/default_value_pref_gallery_slideshow_transition" />
+
+ <CheckBoxPreference
+ android:key="pref_gallery_slideshow_repeat_key"
+ android:title="@string/pref_gallery_slideshow_repeat_title"
+ android:summary="@string/pref_gallery_slideshow_repeat_summary"
+ android:defaultValue="false"/>
+
+ <CheckBoxPreference
+ android:key="pref_gallery_slideshow_shuffle_key"
+ android:title="@string/pref_gallery_slideshow_shuffle_title"
+ android:summary="@string/pref_gallery_slideshow_shuffle_summary"
+ android:defaultValue="false"/>
+
+ </PreferenceCategory>
+</PreferenceScreen>
diff --git a/src/com/android/camera/BufferedInputStream.java b/src/com/android/camera/BufferedInputStream.java
new file mode 100644
index 0000000..1505e87
--- /dev/null
+++ b/src/com/android/camera/BufferedInputStream.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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;
+/*
+* This file derives from the Apache version of the BufferedInputStream.
+* Mods to support passing in the buffer rather than creating one directly.
+*/
+import java.io.InputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+
+public class BufferedInputStream extends FilterInputStream {
+ protected byte[] buf;
+ protected int count;
+ protected int marklimit;
+ protected int markpos = -1;
+ protected int pos;
+
+ private boolean closed = false;
+
+ public BufferedInputStream(InputStream in, byte [] buffer) {
+ super(in);
+ buf = buffer;
+ if (buf == null) {
+ throw new java.security.InvalidParameterException();
+ }
+ }
+
+ @Override
+ public synchronized int available() throws IOException {
+ return count - pos + in.available();
+ }
+
+ @Override
+ public synchronized void close() throws IOException {
+ if (null != in) {
+ super.close();
+ in = null;
+ }
+ buf = null;
+ closed = true;
+ }
+
+ private int fillbuf() throws IOException {
+ if (markpos == -1 || (pos - markpos >= marklimit)) {
+ /* Mark position not set or exceeded readlimit */
+ int result = in.read(buf);
+ if (result > 0) {
+ markpos = -1;
+ pos = 0;
+ count = result == -1 ? 0 : result;
+ }
+ return result;
+ }
+ if (markpos == 0 && marklimit > buf.length) {
+ /* Increase buffer size to accomodate the readlimit */
+ int newLength = buf.length * 2;
+ if (newLength > marklimit) {
+ newLength = marklimit;
+ }
+ byte[] newbuf = new byte[newLength];
+ System.arraycopy(buf, 0, newbuf, 0, buf.length);
+ buf = newbuf;
+ } else if (markpos > 0) {
+ System.arraycopy(buf, markpos, buf, 0, buf.length - markpos);
+ }
+ /* Set the new position and mark position */
+ pos -= markpos;
+ count = markpos = 0;
+ int bytesread = in.read(buf, pos, buf.length - pos);
+ count = bytesread <= 0 ? pos : pos + bytesread;
+ return bytesread;
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ marklimit = readlimit;
+ markpos = pos;
+ }
+
+ @Override
+ public boolean markSupported() {
+ return true;
+ }
+
+ @Override
+ public synchronized int read() throws IOException {
+ if (in == null) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+
+ /* Are there buffered bytes available? */
+ if (pos >= count && fillbuf() == -1) {
+ return -1; /* no, fill buffer */
+ }
+
+ /* Did filling the buffer fail with -1 (EOF)? */
+ if (count - pos > 0) {
+ return buf[pos++] & 0xFF;
+ }
+ return -1;
+ }
+
+ @Override
+ public synchronized int read(byte[] buffer, int offset, int length)
+ throws IOException {
+ if (closed) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ // avoid int overflow
+ if (offset > buffer.length - length || offset < 0 || length < 0) {
+ throw new IndexOutOfBoundsException();
+ }
+ if (length == 0) {
+ return 0;
+ }
+ if (null == buf) {
+ throw new IOException(); //$NON-NLS-1$
+ }
+
+ int required;
+ if (pos < count) {
+ /* There are bytes available in the buffer. */
+ int copylength = count - pos >= length ? length : count - pos;
+ System.arraycopy(buf, pos, buffer, offset, copylength);
+ pos += copylength;
+ if (copylength == length || in.available() == 0) {
+ return copylength;
+ }
+ offset += copylength;
+ required = length - copylength;
+ } else {
+ required = length;
+ }
+
+ while (true) {
+ int read;
+ /*
+ * If we're not marked and the required size is greater than the
+ * buffer, simply read the bytes directly bypassing the buffer.
+ */
+ if (markpos == -1 && required >= buf.length) {
+ read = in.read(buffer, offset, required);
+ if (read == -1) {
+ return required == length ? -1 : length - required;
+ }
+ } else {
+ if (fillbuf() == -1) {
+ return required == length ? -1 : length - required;
+ }
+ read = count - pos >= required ? required : count - pos;
+ System.arraycopy(buf, pos, buffer, offset, read);
+ pos += read;
+ }
+ required -= read;
+ if (required == 0) {
+ return length;
+ }
+ if (in.available() == 0) {
+ return length - required;
+ }
+ offset += read;
+ }
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ if (closed) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ if (-1 == markpos) {
+ // K005a=Mark has been invalidated.
+ throw new IOException(); //$NON-NLS-1$
+ }
+ pos = markpos;
+ }
+
+ @Override
+ public synchronized long skip(long amount) throws IOException {
+ if (null == in) {
+ // K0059=Stream is closed
+ throw new IOException(); //$NON-NLS-1$
+ }
+ if (amount < 1) {
+ return 0;
+ }
+
+ if (count - pos >= amount) {
+ pos += amount;
+ return amount;
+ }
+ long read = count - pos;
+ pos = count;
+
+ if (markpos != -1) {
+ if (amount <= marklimit) {
+ if (fillbuf() == -1) {
+ return read;
+ }
+ if (count - pos >= amount - read) {
+ pos += amount - read;
+ return amount;
+ }
+ // Couldn't get all the bytes, skip what we read
+ read += (count - pos);
+ pos = count;
+ return read;
+ }
+ markpos = -1;
+ }
+ return read + in.skip(amount - read);
+ }
+}
diff --git a/src/com/android/camera/Camera.java b/src/com/android/camera/Camera.java
new file mode 100644
index 0000000..ef99842
--- /dev/null
+++ b/src/com/android/camera/Camera.java
@@ -0,0 +1,1527 @@
+/*
+ * 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 android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.graphics.drawable.ColorDrawable;
+import android.hardware.Camera.PictureCallback;
+import android.hardware.Camera.Size;
+import android.location.Location;
+import android.location.LocationManager;
+import android.location.LocationProvider;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.StatFs;
+import android.os.SystemClock;
+import android.pim.DateFormat;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.Video;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.OrientationListener;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class Camera extends Activity implements View.OnClickListener, SurfaceHolder.Callback {
+
+ private static final String TAG = "camera";
+
+ private static final boolean DEBUG = false;
+
+ private static final int CROP_MSG = 1;
+ private static final int KEEP = 2;
+ private static final int RESTART_PREVIEW = 3;
+ private static final int CLEAR_SCREEN_DELAY = 4;
+
+ private static final int SCREEN_DELAY = 2 * 60 * 1000;
+ private static final int FOCUS_BEEP_VOLUME = 100;
+
+ private static final int NO_STORAGE_ERROR = -1;
+ private static final int CANNOT_STAT_ERROR = -2;
+
+ public static final int MENU_SWITCH_TO_VIDEO = 0;
+ public static final int MENU_FLASH_SETTING = 1;
+ public static final int MENU_FLASH_AUTO = 2;
+ public static final int MENU_FLASH_ON = 3;
+ public static final int MENU_FLASH_OFF = 4;
+ public static final int MENU_SETTINGS = 5;
+ public static final int MENU_GALLERY_PHOTOS = 6;
+ public static final int MENU_SAVE_SELECT_PHOTOS = 30;
+ public static final int MENU_SAVE_NEW_PHOTO = 31;
+ public static final int MENU_SAVE_SELECTVIDEO = 32;
+ public static final int MENU_SAVE_TAKE_NEW_VIDEO = 33;
+ public static final int MENU_SAVE_GALLERY_PHOTO = 34;
+ public static final int MENU_SAVE_GALLERY_VIDEO_PHOTO = 35;
+ public static final int MENU_SAVE_CAMERA_DONE = 36;
+ public static final int MENU_SAVE_CAMERA_VIDEO_DONE = 37;
+
+ Toast mToast;
+ OrientationListener mOrientationListener;
+ int mLastOrientation = OrientationListener.ORIENTATION_UNKNOWN;
+ SharedPreferences mPreferences;
+
+ static final int IDLE = 1;
+ static final int SNAPSHOT_IN_PROGRESS = 2;
+ static final int SNAPSHOT_COMPLETED = 3;
+
+ int mStatus = IDLE;
+
+ android.hardware.Camera mCameraDevice;
+ SurfaceView mSurfaceView;
+ SurfaceHolder mSurfaceHolder = null;
+ ImageView mBlackout = null;
+
+ int mViewFinderWidth, mViewFinderHeight;
+ boolean mPreviewing = false;
+
+ MediaPlayer mClickSound;
+
+ Capturer mCaptureObject;
+ ImageCapture mImageCapture = null;
+
+ boolean mPausing = false;
+
+ boolean mIsFocusing = false;
+ boolean mIsFocused = false;
+ boolean mIsFocusButtonPressed = false;
+ boolean mCaptureOnFocus = false;
+
+ static ContentResolver mContentResolver;
+ boolean mDidRegister = false;
+
+ int mCurrentZoomIndex = 0;
+
+ private static final int STILL_MODE = 1;
+ private static final int VIDEO_MODE = 2;
+ private int mMode = STILL_MODE;
+
+ ArrayList<MenuItem> mGalleryItems = new ArrayList<MenuItem>();
+
+ boolean mMenuSelectionMade;
+
+ View mPostPictureAlert;
+ LocationManager mLocationManager = null;
+
+ private Animation mFocusBlinkAnimation;
+ private View mFocusIndicator;
+ private ToneGenerator mFocusToneGenerator;
+
+ private ShutterCallback mShutterCallback = new ShutterCallback();
+ private RawPictureCallback mRawPictureCallback = new RawPictureCallback();
+ private JpegPictureCallback mJpegPictureCallback = new JpegPictureCallback();
+ private AutoFocusCallback mAutoFocusCallback = new AutoFocusCallback();
+ private long mShutterPressTime;
+ private int mPicturesRemaining;
+
+ private boolean mKeepAndRestartPreview;
+
+ private Handler mHandler = new MainHandler();
+ private ProgressDialog mSavingProgress;
+
+ interface Capturer {
+ Uri getLastCaptureUri();
+ void onSnap();
+ void dismissFreezeFrame(boolean keep);
+ void cancelSave();
+ void cancelAutoDismiss();
+ void setDone(boolean wait);
+ }
+
+ private void cancelSavingNotification() {
+ if (mToast != null) {
+ mToast.cancel();
+ mToast = null;
+ }
+ }
+
+ /** This Handler is used to post message back onto the main thread of the application */
+ private class MainHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case KEEP: {
+ keep();
+ if (mSavingProgress != null) {
+ mSavingProgress.cancel();
+ mSavingProgress = null;
+ }
+
+ mKeepAndRestartPreview = true;
+
+ if (msg.obj != null) {
+ mHandler.post((Runnable)msg.obj);
+ }
+ break;
+ }
+
+ case RESTART_PREVIEW: {
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ // We are still in the processing of taking the picture, wait.
+ // This is is strange. Why are we polling?
+ // TODO remove polling
+ mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, 100);
+ } else if (mStatus == SNAPSHOT_COMPLETED){
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ break;
+ }
+
+ case CLEAR_SCREEN_DELAY: {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ break;
+ }
+ }
+ }
+ };
+
+ LocationListener [] mLocationListeners = new LocationListener[] {
+ new LocationListener(LocationManager.GPS_PROVIDER),
+ new LocationListener(LocationManager.NETWORK_PROVIDER)
+ };
+
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ showStorageToast();
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ showStorageToast();
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(Camera.this, getResources().getString(R.string.wait), 5000);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ showStorageToast();
+ }
+ }
+ };
+
+ private class LocationListener implements android.location.LocationListener {
+ Location mLastLocation;
+ boolean mValid = false;
+ String mProvider;
+
+ public LocationListener(String provider) {
+ mProvider = provider;
+ mLastLocation = new Location(mProvider);
+ }
+
+ public void onLocationChanged(Location newLocation) {
+ if (newLocation.getLatitude() == 0.0 && newLocation.getLongitude() == 0.0) {
+ // Hack to filter out 0.0,0.0 locations
+ return;
+ }
+ mLastLocation.set(newLocation);
+ mValid = true;
+ }
+
+ public void onProviderEnabled(String provider) {
+ }
+
+ public void onProviderDisabled(String provider) {
+ mValid = false;
+ }
+
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ if (status == LocationProvider.OUT_OF_SERVICE) {
+ mValid = false;
+ }
+ }
+
+ public Location current() {
+ return mValid ? mLastLocation : null;
+ }
+ };
+
+ private long mRawPictureCallbackTime;
+
+ private boolean mImageSavingItem = false;
+
+ private final class ShutterCallback implements android.hardware.Camera.ShutterCallback {
+ public void onShutter() {
+ if (DEBUG) {
+ long now = System.currentTimeMillis();
+ Log.v(TAG, "********** Total shutter lag " + (now - mShutterPressTime) + " ms");
+ }
+ if (mClickSound != null) {
+ mClickSound.seekTo(0);
+ mClickSound.start();
+ }
+ }
+ };
+
+ private final class RawPictureCallback implements PictureCallback {
+ public void onPictureTaken(byte [] rawData, android.hardware.Camera camera) {
+ if (Config.LOGV)
+ Log.v(TAG, "got RawPictureCallback...");
+ mRawPictureCallbackTime = System.currentTimeMillis();
+ mBlackout.setVisibility(View.INVISIBLE);
+ if (!isPickIntent() && mPreferences.getBoolean("pref_camera_postpicturemenu_key", true)) {
+ mPostPictureAlert.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+
+ private final class JpegPictureCallback implements PictureCallback {
+ public void onPictureTaken(byte [] jpegData, android.hardware.Camera camera) {
+ if (Config.LOGV)
+ Log.v(TAG, "got JpegPictureCallback...");
+
+ mImageCapture.storeImage(jpegData, camera);
+
+ mStatus = SNAPSHOT_COMPLETED;
+
+ if (!mPreferences.getBoolean("pref_camera_postpicturemenu_key", true)) {
+ if (mKeepAndRestartPreview) {
+ long delay = 1500 - (System.currentTimeMillis() - mRawPictureCallbackTime);
+ mHandler.sendEmptyMessageDelayed(RESTART_PREVIEW, Math.max(delay, 0));
+ }
+ return;
+ }
+
+ if (mKeepAndRestartPreview) {
+ mKeepAndRestartPreview = false;
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+
+ // Post this message so that we can finish processing the request. This also
+ // prevents the preview from showing up before mPostPictureAlert is dismissed.
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ }
+
+ }
+ };
+
+ private final class AutoFocusCallback implements android.hardware.Camera.AutoFocusCallback {
+ public void onAutoFocus(boolean focused, android.hardware.Camera camera) {
+ mIsFocusing = false;
+ mIsFocused = focused;
+ if (focused) {
+ if (mCaptureOnFocus && mCaptureObject != null) {
+ // No need to play the AF sound if we're about to play the shutter sound
+ mCaptureObject.onSnap();
+ clearFocus();
+ } else {
+ mFocusToneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP2);
+ }
+ mCaptureOnFocus = false;
+ }
+ updateFocusIndicator();
+ }
+ };
+
+ private class ImageCapture implements Capturer {
+
+ private boolean mCancel = false;
+ private boolean mCapturing = false;
+
+ private Uri mLastContentUri;
+ private ImageManager.IAddImage_cancelable mAddImageCancelable;
+
+ Bitmap mCaptureOnlyBitmap;
+
+ /** These member variables are used for various debug timings */
+ private long mThreadTimeStart;
+ private long mThreadTimeEnd;
+ private long mWallTimeStart;
+ private long mWallTimeEnd;
+
+
+ public ImageCapture() {
+ }
+
+ /**
+ * This method sets whether or not we are capturing a picture. This method must be called
+ * with the ImageCapture.this lock held.
+ */
+ public void setCapturingLocked(boolean capturing) {
+ mCapturing = capturing;
+ }
+
+ /*
+ * Tell the ImageCapture thread to exit when possible.
+ */
+ public void setDone(boolean wait) {
+ }
+
+ /*
+ * Tell the image capture thread to not "dismiss" the current
+ * capture when the current image is stored, etc.
+ */
+ public void cancelAutoDismiss() {
+ }
+
+ public void dismissFreezeFrame(boolean keep) {
+ if (keep) {
+ cancelSavingNotification();
+ } else {
+ Toast.makeText(Camera.this, R.string.camera_tossing, Toast.LENGTH_SHORT).show();
+ }
+
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ // If we are still in the process of taking a picture, then just post a message.
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ } else {
+ restartPreview();
+ }
+ }
+
+ private void startTiming() {
+ mWallTimeStart = SystemClock.elapsedRealtime();
+ mThreadTimeStart = Debug.threadCpuTimeNanos();
+ }
+
+ private void stopTiming() {
+ mThreadTimeEnd = Debug.threadCpuTimeNanos();
+ mWallTimeEnd = SystemClock.elapsedRealtime();
+ }
+
+ private void storeImage(byte[] data) {
+ try {
+ if (DEBUG) {
+ startTiming();
+ }
+
+ mLastContentUri = ImageManager.instance().addImage(
+ Camera.this,
+ mContentResolver,
+ DateFormat.format("yyyy-MM-dd kk.mm.ss", System.currentTimeMillis()).toString(),
+ "",
+ System.currentTimeMillis(),
+ // location for the database goes here
+ null,
+ 0, // the dsp will use the right orientation so don't "double set it"
+ ImageManager.CAMERA_IMAGE_BUCKET_NAME,
+ null);
+
+ if (mLastContentUri == null) {
+ // this means we got an error
+ mCancel = true;
+ }
+ if (!mCancel) {
+ mAddImageCancelable = ImageManager.instance().storeImage(mLastContentUri,
+ Camera.this, mContentResolver, 0, null, data);
+ mAddImageCancelable.get();
+ mAddImageCancelable = null;
+ }
+
+ if (DEBUG) {
+ stopTiming();
+ Log.d(TAG, "Storing image took " + (mWallTimeEnd - mWallTimeStart) + " ms. " +
+ "Thread time was " + ((mThreadTimeEnd - mThreadTimeStart) / 1000000) +
+ " ms.");
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "Exception while compressing image.", ex);
+ }
+ }
+
+ public void storeImage(byte[] data, android.hardware.Camera camera) {
+ boolean captureOnly = isPickIntent();
+
+ if (!captureOnly) {
+ storeImage(data);
+ sendBroadcast(new Intent("com.android.camera.NEW_PICTURE", mLastContentUri));
+ } else {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = 4;
+
+ if (DEBUG) {
+ startTiming();
+ }
+
+ mCaptureOnlyBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, options);
+
+ if (DEBUG) {
+ stopTiming();
+ Log.d(TAG, "Decoded mCaptureOnly bitmap (" + mCaptureOnlyBitmap.getWidth() +
+ "x" + mCaptureOnlyBitmap.getHeight() + " ) in " +
+ (mWallTimeEnd - mWallTimeStart) + " ms. Thread time was " +
+ ((mThreadTimeEnd - mThreadTimeStart) / 1000000) + " ms.");
+ }
+
+ openOptionsMenu();
+ }
+
+
+ mCapturing = false;
+ if (mPausing) {
+ closeCamera();
+ }
+ }
+
+ /*
+ * Tells the image capture thread to abort the capture of the
+ * current image.
+ */
+ public void cancelSave() {
+ if (!mCapturing) {
+ return;
+ }
+
+ mCancel = true;
+
+ if (mAddImageCancelable != null) {
+ mAddImageCancelable.cancel();
+ }
+ dismissFreezeFrame(false);
+ }
+
+ /*
+ * Initiate the capture of an image.
+ */
+ public void initiate(boolean captureOnly) {
+ if (mCameraDevice == null) {
+ return;
+ }
+
+ mCancel = false;
+ mCapturing = true;
+
+ capture(captureOnly);
+ }
+
+ public Uri getLastCaptureUri() {
+ return mLastContentUri;
+ }
+
+ public Bitmap getLastBitmap() {
+ return mCaptureOnlyBitmap;
+ }
+
+ private void capture(boolean captureOnly) {
+ mPreviewing = false;
+ mCaptureOnlyBitmap = null;
+
+ final int latchedOrientation = ImageManager.roundOrientation(mLastOrientation + 90);
+
+ Boolean recordLocation = mPreferences.getBoolean("pref_camera_recordlocation_key", false);
+ Location loc = recordLocation ? getCurrentLocation() : null;
+ android.hardware.Camera.Parameters parameters = mCameraDevice.getParameters();
+ // Quality 75 has visible artifacts, and quality 90 looks great but the files begin to
+ // get large. 85 is a good compromise between the two.
+ parameters.set("jpeg-quality", 85);
+ parameters.set("rotation", latchedOrientation);
+ if (loc != null) {
+ parameters.set("gps-latitude", String.valueOf(loc.getLatitude()));
+ parameters.set("gps-longitude", String.valueOf(loc.getLongitude()));
+ parameters.set("gps-altitude", String.valueOf(loc.getAltitude()));
+ parameters.set("gps-timestamp", String.valueOf(loc.getTime()));
+ } else {
+ parameters.remove("gps-latitude");
+ parameters.remove("gps-longitude");
+ parameters.remove("gps-altitude");
+ parameters.remove("gps-timestamp");
+ }
+
+ Size pictureSize = parameters.getPictureSize();
+ Size previewSize = parameters.getPreviewSize();
+
+ // resize the SurfaceView to the aspect-ratio of the still image
+ // and so that we can see the full image that was taken
+ ViewGroup.LayoutParams lp = mSurfaceView.getLayoutParams();
+ if (pictureSize.width*previewSize.height < previewSize.width*pictureSize.height) {
+ lp.width = (pictureSize.width * previewSize.height) / pictureSize.height;
+ } else {
+ lp.height = (pictureSize.height * previewSize.width) / pictureSize.width;
+ }
+ mSurfaceView.requestLayout();
+
+ mCameraDevice.setParameters(parameters);
+
+ mCameraDevice.takePicture(mShutterCallback, mRawPictureCallback, mJpegPictureCallback);
+
+ mBlackout.setVisibility(View.VISIBLE);
+ // Comment this out for now until we can decode the preview frame. This currently
+ // just animates black-on-black because the surface flinger blacks out the surface
+ // when the camera driver detaches the buffers.
+ if (false) {
+ Animation a = new android.view.animation.TranslateAnimation(mBlackout.getWidth(), 0 , 0, 0);
+ a.setDuration(450);
+ a.startNow();
+ mBlackout.setAnimation(a);
+ }
+ }
+
+ public void onSnap() {
+ // If we are already in the middle of taking a snapshot then we should just save
+ // the image after we have returned from the camera service.
+ if (mStatus == SNAPSHOT_IN_PROGRESS || mStatus == SNAPSHOT_COMPLETED) {
+ mKeepAndRestartPreview = true;
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ return;
+ }
+
+ // Don't check the filesystem here, we can't afford the latency. Instead, check the
+ // cached value which was calculated when the preview was restarted.
+ if (DEBUG) mShutterPressTime = System.currentTimeMillis();
+ if (mPicturesRemaining < 1) {
+ showStorageToast();
+ return;
+ }
+
+ mStatus = SNAPSHOT_IN_PROGRESS;
+
+ mKeepAndRestartPreview = !mPreferences.getBoolean("pref_camera_postpicturemenu_key", true);
+
+ boolean getContentAction = isPickIntent();
+ if (getContentAction) {
+ mImageCapture.initiate(true);
+ } else {
+ mImageCapture.initiate(false);
+ }
+ }
+ }
+
+ static public Matrix GetDisplayMatrix(Bitmap b, ImageView v) {
+ Matrix m = new Matrix();
+ float bw = (float)b.getWidth();
+ float bh = (float)b.getHeight();
+ float vw = (float)v.getWidth();
+ float vh = (float)v.getHeight();
+ float scale, x, y;
+ if (bw*vh > vw*bh) {
+ scale = vh / bh;
+ x = (vw - scale*bw)*0.5F;
+ y = 0;
+ } else {
+ scale = vw / bw;
+ x = 0;
+ y = (vh - scale*bh)*0.5F;
+ }
+ m.setScale(scale, scale, 0.5F, 0.5F);
+ m.postTranslate(x, y);
+ return m;
+ }
+
+ private void postAfterKeep(final Runnable r) {
+ Resources res = getResources();
+
+ if (mSavingProgress != null) {
+ mSavingProgress = ProgressDialog.show(this, res.getString(R.string.savingImage),
+ res.getString(R.string.wait));
+ }
+
+ Message msg = mHandler.obtainMessage(KEEP);
+ msg.obj = r;
+ msg.sendToTarget();
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mLocationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ mContentResolver = getContentResolver();
+
+ //setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ requestWindowFeature(Window.FEATURE_PROGRESS);
+
+ Window win = getWindow();
+ win.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ setContentView(R.layout.camera);
+
+ mSurfaceView = (SurfaceView) findViewById(R.id.camera_preview);
+
+ // don't set mSurfaceHolder here. We have it set ONLY within
+ // surfaceCreated / surfaceDestroyed, other parts of the code
+ // assume that when it is set, the surface is also set.
+ SurfaceHolder holder = mSurfaceView.getHolder();
+ holder.addCallback(this);
+ holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+ mBlackout = (ImageView) findViewById(R.id.blackout);
+ mBlackout.setBackgroundDrawable(new ColorDrawable(0xFF000000));
+
+ mPostPictureAlert = findViewById(R.id.post_picture_panel);
+ View b;
+
+ b = findViewById(R.id.save);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.discard);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.share);
+ b.setOnClickListener(this);
+
+ b = findViewById(R.id.setas);
+ b.setOnClickListener(this);
+
+ try {
+ mClickSound = new MediaPlayer();
+ AssetFileDescriptor afd = getResources().openRawResourceFd(R.raw.camera_click);
+
+ mClickSound.setDataSource(afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getLength());
+
+ if (mClickSound != null) {
+ mClickSound.setAudioStreamType(AudioManager.STREAM_SYSTEM);
+ mClickSound.prepare();
+ }
+ } catch (Exception ex) {
+ Log.w(TAG, "Couldn't create click sound", ex);
+ }
+
+ mOrientationListener = new OrientationListener(this) {
+ public void onOrientationChanged(int orientation) {
+ mLastOrientation = orientation;
+ }
+ };
+
+ mFocusIndicator = findViewById(R.id.focus_indicator);
+ mFocusBlinkAnimation = AnimationUtils.loadAnimation(this, R.anim.auto_focus_blink);
+ mFocusBlinkAnimation.setRepeatCount(Animation.INFINITE);
+ mFocusBlinkAnimation.setRepeatMode(Animation.REVERSE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ final View hintView = findViewById(R.id.hint_toast);
+ if (hintView != null)
+ hintView.setVisibility(View.GONE);
+
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ final boolean storageOK = calculatePicturesRemaining() > 0;
+ if (hintView == null)
+ return;
+
+ if (storageOK) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ hintView.setVisibility(View.VISIBLE);
+ }
+ });
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ Animation a = new android.view.animation.AlphaAnimation(1F, 0F);
+ a.setDuration(500);
+ a.startNow();
+ hintView.setAnimation(a);
+ hintView.setVisibility(View.GONE);
+ }
+ }, 3000);
+ } else {
+ mHandler.post(new Runnable() {
+ public void run() {
+ hintView.setVisibility(View.GONE);
+ showStorageToast();
+ }
+ });
+ }
+ }
+ });
+ t.start();
+ }
+
+ public void onClick(View v) {
+ switch (v.getId()) {
+ case R.id.save: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(null);
+ break;
+ }
+
+ case R.id.discard: {
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ Uri uri = mCaptureObject.getLastCaptureUri();
+ if (uri != null) {
+ mContentResolver.delete(uri, null, null);
+ }
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ break;
+ }
+
+ case R.id.share: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(new Runnable() {
+ public void run() {
+ Uri u = mCaptureObject.getLastCaptureUri();
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType("image/jpeg");
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ startActivity(Intent.createChooser(intent, getText(R.string.sendImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(Camera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ break;
+ }
+
+ case R.id.setas: {
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ postAfterKeep(new Runnable() {
+ public void run() {
+ Uri u = mCaptureObject.getLastCaptureUri();
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA, u);
+ try {
+ startActivity(Intent.createChooser(intent, getText(R.string.setImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(Camera.this, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ break;
+ }
+ }
+ }
+
+ void keepVideo() {
+ };
+
+ private void showStorageToast() {
+ String noStorageText = null;
+ int remaining = calculatePicturesRemaining();
+
+ if (remaining == NO_STORAGE_ERROR) {
+ noStorageText = getString(R.string.no_storage);
+ } else if (remaining < 1) {
+ noStorageText = getString(R.string.not_enough_space);
+ }
+
+ if (noStorageText != null) {
+ Toast.makeText(this, noStorageText, 5000).show();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+
+ mPausing = false;
+ mOrientationListener.enable();
+
+ if (isPickIntent()) {
+ mMode = STILL_MODE;
+ }
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addDataScheme("file");
+ registerReceiver(mReceiver, intentFilter);
+ mDidRegister = true;
+
+ mImageCapture = new ImageCapture();
+
+ restartPreview();
+
+ if (mPreferences.getBoolean("pref_camera_recordlocation_key", false))
+ startReceivingLocationUpdates();
+
+ updateFocusIndicator();
+
+ mFocusToneGenerator = new ToneGenerator(AudioManager.STREAM_SYSTEM, FOCUS_BEEP_VOLUME);
+
+ mBlackout.setVisibility(View.INVISIBLE);
+ }
+
+ private ImageManager.DataLocation dataLocation() {
+ return ImageManager.DataLocation.EXTERNAL;
+ }
+
+ @Override
+ public void onStop() {
+ keep();
+ stopPreview();
+ closeCamera();
+ mHandler.removeMessages(CLEAR_SCREEN_DELAY);
+ super.onStop();
+ }
+
+ @Override
+ protected void onPause() {
+ keep();
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+
+ mPausing = true;
+ mOrientationListener.disable();
+
+ stopPreview();
+
+ if (!mImageCapture.mCapturing) {
+ closeCamera();
+ }
+ if (mDidRegister) {
+ unregisterReceiver(mReceiver);
+ mDidRegister = false;
+ }
+ stopReceivingLocationUpdates();
+ mFocusToneGenerator.release();
+ mFocusToneGenerator = null;
+
+ super.onPause();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case CROP_MSG: {
+ Intent intent = new Intent();
+ if (data != null) {
+ Bundle extras = data.getExtras();
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ }
+ setResult(resultCode, intent);
+ finish();
+ break;
+ }
+ }
+ }
+
+ private void autoFocus() {
+ updateFocusIndicator();
+ if (!mIsFocusing) {
+ if (mCameraDevice != null) {
+ mIsFocusing = true;
+ mIsFocused = false;
+ mCameraDevice.autoFocus(mAutoFocusCallback);
+ }
+ }
+ }
+
+ private void clearFocus() {
+ mIsFocusing = false;
+ mIsFocused = false;
+ mIsFocusButtonPressed = false;
+ }
+
+ private void updateFocusIndicator() {
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mIsFocusing || !mIsFocusButtonPressed) {
+ mFocusIndicator.setVisibility(View.GONE);
+ mFocusIndicator.clearAnimation();
+ } else {
+ if (mIsFocused) {
+ mFocusIndicator.setVisibility(View.VISIBLE);
+ mFocusIndicator.clearAnimation();
+ } else {
+ mFocusIndicator.setVisibility(View.VISIBLE);
+ mFocusIndicator.startAnimation(mFocusBlinkAnimation);
+ }
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ mHandler.sendEmptyMessageDelayed(CLEAR_SCREEN_DELAY, SCREEN_DELAY);
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ if (mStatus == SNAPSHOT_IN_PROGRESS || mStatus == SNAPSHOT_COMPLETED) {
+ if (mPostPictureAlert.getVisibility() == View.VISIBLE) {
+ keep();
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ restartPreview();
+ }
+ // ignore backs while we're taking a picture
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_FOCUS:
+ mIsFocusButtonPressed = true;
+ if (event.getRepeatCount() == 0) {
+ if (mPreviewing) {
+ autoFocus();
+ } else if (mCaptureObject != null) {
+ // Save and restart preview
+ mCaptureObject.onSnap();
+ }
+ }
+ return true;
+ case KeyEvent.KEYCODE_CAMERA:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (event.getRepeatCount() == 0) {
+ // The camera operates in focus-priority mode, meaning that we take a picture
+ // when focusing completes, and only if it completes successfully. If the user
+ // has half-pressed the shutter and already locked focus, we can take the photo
+ // right away, otherwise we need to start AF.
+ if (mIsFocused || !mPreviewing) {
+ // doesn't get set until the idler runs
+ if (mCaptureObject != null) {
+ mCaptureObject.onSnap();
+ }
+ clearFocus();
+ updateFocusIndicator();
+ } else {
+ // Half pressing the shutter (i.e. the focus button event) will already have
+ // requested AF for us, so just request capture on focus here. If AF has
+ // already failed, we don't want to trigger it again.
+ mCaptureOnFocus = true;
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && !mIsFocusButtonPressed) {
+ // But we do need to start AF for DPAD_CENTER
+ autoFocus();
+ }
+ }
+ }
+ return true;
+ case KeyEvent.KEYCODE_MENU:
+ mPostPictureAlert.setVisibility(View.INVISIBLE);
+ break;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_FOCUS:
+ clearFocus();
+ updateFocusIndicator();
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
+ if (mMode == STILL_MODE) {
+ // if we're creating the surface, start the preview as well.
+ boolean preview = holder.isCreating();
+ setViewFinder(w, h, preview);
+ mCaptureObject = mImageCapture;
+ }
+ }
+
+ public void surfaceCreated(SurfaceHolder holder) {
+ mSurfaceHolder = holder;
+ }
+
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stopPreview();
+ mSurfaceHolder = null;
+ }
+
+ private void closeCamera() {
+ if (mCameraDevice != null) {
+ mCameraDevice.release();
+ mCameraDevice = null;
+ mPreviewing = false;
+ }
+ }
+
+ private boolean ensureCameraDevice() {
+ if (mCameraDevice == null) {
+ mCameraDevice = android.hardware.Camera.open();
+ }
+ return mCameraDevice != null;
+ }
+
+ private void restartPreview() {
+ SurfaceView surfaceView = mSurfaceView;
+ if (surfaceView == null ||
+ surfaceView.getWidth() == 0 || surfaceView.getHeight() == 0) {
+ return;
+ }
+ // make sure the surfaceview fills the whole screen when previewing
+ ViewGroup.LayoutParams lp = surfaceView.getLayoutParams();
+ lp.width = ViewGroup.LayoutParams.FILL_PARENT;
+ lp.height = ViewGroup.LayoutParams.FILL_PARENT;
+ surfaceView.requestLayout();
+ setViewFinder(mViewFinderWidth, mViewFinderHeight, true);
+ mStatus = IDLE;
+
+ // Calculate this in advance of each shot so we don't add to shutter latency. It's true that
+ // someone else could write to the SD card in the mean time and fill it, but that could have
+ // happened between the shutter press and saving the JPEG too.
+ // TODO: The best longterm solution is to write a reserve file of maximum JPEG size, always
+ // let the user take a picture, and delete that file if needed to save the new photo.
+ calculatePicturesRemaining();
+ }
+
+ private void setViewFinder(int w, int h, boolean startPreview) {
+ if (mPausing)
+ return;
+
+ if (mPreviewing &&
+ w == mViewFinderWidth &&
+ h == mViewFinderHeight) {
+ return;
+ }
+
+ if (!ensureCameraDevice())
+ return;
+
+ if (mSurfaceHolder == null)
+ return;
+
+ if (isFinishing())
+ return;
+
+ if (mPausing)
+ return;
+
+ // remember view finder size
+ mViewFinderWidth = w;
+ mViewFinderHeight = h;
+
+ if (startPreview == false)
+ return;
+
+ /*
+ * start the preview if we're asked to...
+ */
+
+ // we want to start the preview and we're previewing already,
+ // stop the preview first (this will blank the screen).
+ if (mPreviewing)
+ stopPreview();
+
+ // this blanks the screen if the surface changed, no-op otherwise
+ mCameraDevice.setPreviewDisplay(mSurfaceHolder);
+
+
+ // request the preview size, the hardware may not honor it,
+ // if we depended on it we would have to query the size again
+ android.hardware.Camera.Parameters p = mCameraDevice.getParameters();
+ p.setPreviewSize(w, h);
+ mCameraDevice.setParameters(p);
+
+
+ final long wallTimeStart = SystemClock.elapsedRealtime();
+ final long threadTimeStart = Debug.threadCpuTimeNanos();
+
+ final Object watchDogSync = new Object();
+ Thread watchDog = new Thread(new Runnable() {
+ public void run() {
+ int next_warning = 1;
+ while (true) {
+ try {
+ synchronized (watchDogSync) {
+ watchDogSync.wait(1000);
+ }
+ } catch (InterruptedException ex) {
+ //
+ }
+ if (mPreviewing)
+ break;
+
+ int delay = (int) (SystemClock.elapsedRealtime() - wallTimeStart) / 1000;
+ if (delay >= next_warning) {
+ if (delay < 120) {
+ Log.e(TAG, "preview hasn't started yet in " + delay + " seconds");
+ } else {
+ Log.e(TAG, "preview hasn't started yet in " + (delay / 60) + " minutes");
+ }
+ if (next_warning < 60) {
+ next_warning <<= 1;
+ if (next_warning == 16) {
+ next_warning = 15;
+ }
+ } else {
+ next_warning += 60;
+ }
+ }
+ }
+ }
+ });
+
+ watchDog.start();
+
+ if (Config.LOGV)
+ Log.v(TAG, "calling mCameraDevice.startPreview");
+ mCameraDevice.startPreview();
+ mPreviewing = true;
+
+ synchronized (watchDogSync) {
+ watchDogSync.notify();
+ }
+
+ long threadTimeEnd = Debug.threadCpuTimeNanos();
+ long wallTimeEnd = SystemClock.elapsedRealtime();
+ if ((wallTimeEnd - wallTimeStart) > 3000) {
+ Log.w(TAG, "startPreview() to " + (wallTimeEnd - wallTimeStart) + " ms. Thread time was"
+ + (threadTimeEnd - threadTimeStart) / 1000000 + " ms.");
+ }
+ }
+
+ private void stopPreview() {
+ if (mCameraDevice != null && mPreviewing) {
+ mCameraDevice.stopPreview();
+ }
+ mPreviewing = false;
+ }
+
+ void gotoGallery() {
+ Uri target = mMode == STILL_MODE ? Images.Media.INTERNAL_CONTENT_URI
+ : Video.Media.INTERNAL_CONTENT_URI;
+ Intent intent = new Intent(Intent.ACTION_VIEW, target);
+ startActivity(intent);
+ }
+
+ void keep() {
+ cancelSavingNotification();
+ if (mCaptureObject != null) {
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ };
+
+ void toss() {
+ cancelSavingNotification();
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ }
+ };
+
+ private ImageManager.IImage getImageForURI(Uri uri) {
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ this,
+ mContentResolver,
+ dataLocation(),
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_ASCENDING);
+ ImageManager.IImage image = list.getImageForUri(uri);
+ list.deactivate();
+ return image;
+ }
+
+
+ private void startReceivingLocationUpdates() {
+ if (mLocationManager != null) {
+ try {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.NETWORK_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[1]);
+ } catch (java.lang.SecurityException ex) {
+ // ok
+ } catch (IllegalArgumentException ex) {
+ if (Config.LOGD) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ }
+ try {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ 1000,
+ 0F,
+ mLocationListeners[0]);
+ } catch (java.lang.SecurityException ex) {
+ // ok
+ } catch (IllegalArgumentException ex) {
+ if (Config.LOGD) {
+ Log.d(TAG, "provider does not exist " + ex.getMessage());
+ }
+ }
+ }
+ }
+
+ private void stopReceivingLocationUpdates() {
+ if (mLocationManager != null) {
+ for (int i = 0; i < mLocationListeners.length; i++) {
+ try {
+ mLocationManager.removeUpdates(mLocationListeners[i]);
+ } catch (Exception ex) {
+ // ok
+ }
+ }
+ }
+ }
+
+ private Location getCurrentLocation() {
+ Location l = null;
+
+ // go in worst to best order
+ for (int i = 0; i < mLocationListeners.length && l == null; i++) {
+ l = mLocationListeners[i].current();
+ }
+
+ return l;
+ }
+
+ @Override
+ public void onOptionsMenuClosed(Menu menu) {
+ super.onOptionsMenuClosed(menu);
+ if (mImageSavingItem && !mMenuSelectionMade) {
+ // save the image if we presented the "advanced" menu
+ // which happens if "menu" is pressed while in
+ // SNAPSHOT_IN_PROGRESS or SNAPSHOT_COMPLETED modes
+ keep();
+ mHandler.sendEmptyMessage(RESTART_PREVIEW);
+ }
+ }
+
+ @Override
+ public boolean onMenuOpened(int featureId, Menu menu) {
+ if (featureId == Window.FEATURE_OPTIONS_PANEL) {
+ if (mStatus == SNAPSHOT_IN_PROGRESS) {
+ mKeepAndRestartPreview = false;
+ mHandler.removeMessages(RESTART_PREVIEW);
+ mMenuSelectionMade = false;
+ }
+ }
+ return super.onMenuOpened(featureId, menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ mMenuSelectionMade = false;
+
+ for (int i = 1; i <= MenuHelper.MENU_ITEM_MAX; i++) {
+ if (i != MenuHelper.GENERIC_ITEM) {
+ menu.setGroupVisible(i, false);
+ }
+ }
+
+ if (mMode == STILL_MODE) {
+ if (mStatus == SNAPSHOT_IN_PROGRESS || mStatus == SNAPSHOT_COMPLETED) {
+ menu.setGroupVisible(MenuHelper.IMAGE_SAVING_ITEM, true);
+ mImageSavingItem = true;
+ } else {
+ menu.setGroupVisible(MenuHelper.IMAGE_MODE_ITEM, true);
+ mImageSavingItem = false;
+ }
+ } else if (mMode == VIDEO_MODE) {
+ }
+
+ if (mCaptureObject != null)
+ mCaptureObject.cancelAutoDismiss();
+
+ return true;
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || MediaStore.ACTION_IMAGE_CAPTURE.equals(action));
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ if (isPickIntent()) {
+ menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_SELECT_PHOTOS , 0, R.string.camera_selectphoto).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Bitmap bitmap = mImageCapture.getLastBitmap();
+ mCaptureObject.setDone(true);
+
+ // TODO scale the image down to something ridiculous until IPC gets straightened out
+ float scale = .5F;
+ Matrix m = new Matrix();
+ m.setScale(scale, scale);
+
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ m, true);
+
+ Bundle myExtras = getIntent().getExtras();
+ String cropValue = myExtras != null ? myExtras.getString("crop") : null;
+ if (cropValue != null) {
+ Bundle newExtras = new Bundle();
+ if (cropValue.equals("circle"))
+ newExtras.putString("circleCrop", "true");
+ newExtras.putParcelable("data", bitmap);
+
+ Intent cropIntent = new Intent();
+ cropIntent.setClass(Camera.this, CropImage.class);
+ cropIntent.putExtras(newExtras);
+ startActivityForResult(cropIntent, CROP_MSG);
+ } else {
+ Bundle extras = new Bundle();
+ extras.putParcelable("data", bitmap);
+ setResult(RESULT_OK, new Intent("inline-data")
+ .putExtra("data", bitmap));
+ finish();
+ }
+ return true;
+ }
+ });
+
+ menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_NEW_PHOTO, 0, R.string.camera_takenewphoto).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ keep();
+ return true;
+ }
+ });
+ menu.add(MenuHelper.VIDEO_SAVING_ITEM, MENU_SAVE_TAKE_NEW_VIDEO, 0, R.string.camera_takenewvideo).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ toss();
+ return true;
+ }
+ });
+ } else {
+ addBaseMenuItems(menu);
+ MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_ROTATE_MENU,
+ Camera.this,
+ mHandler,
+
+ // Handler for deletion
+ new Runnable() {
+ public void run() {
+ if (mCaptureObject != null) {
+ mCaptureObject.cancelSave();
+ Uri uri = mCaptureObject.getLastCaptureUri();
+ if (uri != null) {
+ mContentResolver.delete(uri, null, null);
+ }
+ }
+ }
+ },
+ new MenuHelper.MenuInvoker() {
+ public void run(final MenuHelper.MenuCallback cb) {
+ mMenuSelectionMade = true;
+ postAfterKeep(new Runnable() {
+ public void run() {
+ cb.run(mSelectedImageGetter.getCurrentImageUri(), mSelectedImageGetter.getCurrentImage());
+ if (mCaptureObject != null)
+ mCaptureObject.dismissFreezeFrame(true);
+ }
+ });
+ }
+ });
+
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_SAVING_ITEM, MENU_SAVE_GALLERY_PHOTO, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ postAfterKeep(new Runnable() {
+ public void run() {
+ gotoGallery();
+ }
+ });
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+
+ mGalleryItems.add(menu.add(MenuHelper.VIDEO_SAVING_ITEM, MENU_SAVE_GALLERY_VIDEO_PHOTO, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ keepVideo();
+ gotoGallery();
+ return true;
+ }
+ }));
+ }
+ return true;
+ }
+
+ SelectedImageGetter mSelectedImageGetter =
+ new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return getImageForURI(getCurrentImageUri());
+ }
+ public Uri getCurrentImageUri() {
+ keep();
+ return mCaptureObject.getLastCaptureUri();
+ }
+ };
+
+ private int calculatePicturesRemaining() {
+ try {
+ if (!ImageManager.instance().hasStorage()) {
+ mPicturesRemaining = NO_STORAGE_ERROR;
+ } else {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ float remaining = ((float)stat.getAvailableBlocks() * (float)stat.getBlockSize()) / 400000F;
+ mPicturesRemaining = (int)remaining;
+ }
+ } catch (Exception ex) {
+ // if we can't stat the filesystem then we don't know how many
+ // pictures are remaining. it might be zero but just leave it
+ // blank since we really don't know.
+ mPicturesRemaining = CANNOT_STAT_ERROR;
+ }
+ return mPicturesRemaining;
+ }
+
+ private void addBaseMenuItems(Menu menu) {
+ MenuItem gallery = menu.add(MenuHelper.IMAGE_MODE_ITEM, MENU_GALLERY_PHOTOS, 0, R.string.camera_gallery_photos_text).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ gotoGallery();
+ return true;
+ }
+ });
+ gallery.setIcon(android.R.drawable.ic_menu_gallery);
+ mGalleryItems.add(gallery);
+
+ MenuItem item = menu.add(MenuHelper.GENERIC_ITEM, MENU_SETTINGS, 0, R.string.settings).setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent intent = new Intent();
+ intent.setClass(Camera.this, CameraSettings.class);
+ startActivity(intent);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ }
+}
+
diff --git a/src/com/android/camera/CameraButtonIntentReceiver.java b/src/com/android/camera/CameraButtonIntentReceiver.java
new file mode 100644
index 0000000..ccf5821
--- /dev/null
+++ b/src/com/android/camera/CameraButtonIntentReceiver.java
@@ -0,0 +1,43 @@
+/*
+ * 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 android.content.Context;
+import android.content.Intent;
+import android.content.BroadcastReceiver;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+
+class CameraButtonIntentReceiver extends BroadcastReceiver {
+ public CameraButtonIntentReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+
+ if (event == null) {
+ return;
+ }
+
+ Intent i = new Intent(Intent.ACTION_MAIN);
+ i.setClass(context, Camera.class);
+ i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(i);
+ }
+}
diff --git a/src/com/android/camera/CameraSettings.java b/src/com/android/camera/CameraSettings.java
new file mode 100644
index 0000000..fb77e34
--- /dev/null
+++ b/src/com/android/camera/CameraSettings.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.content.Context;
+
+/**
+ * CameraSettings
+ */
+class CameraSettings extends PreferenceActivity
+{
+ public CameraSettings()
+ {
+ }
+
+ protected int resourceId() {
+ return R.xml.camera_preferences;
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ addPreferencesFromResource(resourceId());
+
+ Preference p = findPreference("pref_camera_upload_albumname_key");
+ if (p != null) {
+ SharedPreferences sp = p.getSharedPreferences();
+ p.setSummary(
+ String.format(getResources().getString(R.string.pref_camera_upload_albumname_summary),
+ p.getSharedPreferences().getString(p.getKey(), UploadService.sUploadAlbumName)));
+ p.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ public boolean onPreferenceChange(Preference p, Object newObjValue) {
+ String newValue = (String) newObjValue;
+ if (newValue == null || newValue.length() == 0)
+ return false;
+ if (android.util.Config.LOGV)
+ android.util.Log.v("camera", "onPreferenceChange ... " + newValue);
+ p.setSummary(String.format(getResources().getString(R.string.pref_camera_upload_albumname_summary), newValue));
+ return true;
+ }
+ });
+ }
+ }
+}
+
diff --git a/src/com/android/camera/CameraThread.java b/src/com/android/camera/CameraThread.java
new file mode 100644
index 0000000..ba888bf
--- /dev/null
+++ b/src/com/android/camera/CameraThread.java
@@ -0,0 +1,95 @@
+/*
+ * 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 android.os.Process;
+
+public class CameraThread {
+ private Thread mThread;
+ private int mTid;
+ private boolean mTidSet;
+ private boolean mFinished;
+
+ synchronized private void setTid(int tid) {
+ mTid = tid;
+ mTidSet = true;
+ CameraThread.this.notifyAll();
+ }
+
+ synchronized private void setFinished() {
+ mFinished = true;
+ }
+
+ public CameraThread(final Runnable r) {
+ Runnable wrapper = new Runnable() {
+ public void run() {
+ setTid(Process.myTid());
+ try {
+ r.run();
+ } finally {
+ setFinished();
+ }
+ }
+ };
+
+ mThread = new Thread(wrapper);
+ }
+
+ synchronized public void start() {
+ mThread.start();
+ }
+
+ synchronized public void setName(String name) {
+ mThread.setName(name);
+ }
+
+ public void join() {
+ try {
+ mThread.join();
+ } catch (InterruptedException ex) {
+ // ok?
+ }
+ }
+
+ public long getId() {
+ return mThread.getId();
+ }
+
+ public Thread realThread() {
+ return mThread;
+ }
+
+ synchronized public void setPriority(int androidOsPriority) {
+ while (!mTidSet) {
+ try {
+ CameraThread.this.wait();
+ } catch (InterruptedException ex) {
+ // ok, try again
+ }
+ }
+ if (!mFinished)
+ Process.setThreadPriority(mTid, androidOsPriority);
+ }
+
+ synchronized public void toBackground() {
+ setPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ }
+
+ synchronized public void toForeground() {
+ setPriority(Process.THREAD_PRIORITY_FOREGROUND);
+ }
+}
diff --git a/src/com/android/camera/CropImage.java b/src/com/android/camera/CropImage.java
new file mode 100644
index 0000000..a7f6404
--- /dev/null
+++ b/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 android.app.Activity;
+import android.app.ProgressDialog;
+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.Paint;
+import android.graphics.PointF;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+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.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+
+public class CropImage extends Activity {
+ private static final String TAG = "CropImage";
+ private ProgressDialog mFaceDetectionDialog = null;
+ private ProgressDialog mSavingProgressDialog = null;
+ private ImageManager.IImageList mAllImages;
+ private Bitmap.CompressFormat mSaveFormat = Bitmap.CompressFormat.JPEG; // only used with mSaveUri
+ private Uri mSaveUri = null;
+ private int mAspectX, mAspectY;
+ private int mOutputX, mOutputY;
+ private boolean mDoFaceDetection = true;
+ private boolean mCircleCrop = false;
+ private boolean mWaitingToPick;
+ private boolean mScale;
+ private boolean mSaving;
+ private boolean mScaleUp = true;
+
+ CropImageView mImageView;
+ ContentResolver mContentResolver;
+
+ Bitmap mBitmap;
+ Bitmap mCroppedImage;
+ HighlightView mCrop;
+
+ ImageManager.IImage mImage;
+
+ public CropImage() {
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ static public class CropImageView extends ImageViewTouchBase {
+ ArrayList<HighlightView> mHighlightViews = new ArrayList<HighlightView>();
+ HighlightView mMotionHighlightView = null;
+ float mLastX, mLastY;
+ int mMotionEdge;
+
+ public CropImageView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected boolean doesScrolling() {
+ return false;
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ if (mBitmapDisplayed != 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);
+ }
+
+ protected void zoomTo(float scale, float centerX, float centerY) {
+ super.zoomTo(scale, centerX, centerY);
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ protected void zoomIn() {
+ super.zoomIn();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+ protected void zoomOut() {
+ super.zoomOut();
+ for (HighlightView hv : mHighlightViews) {
+ hv.mMatrix.set(getImageMatrix());
+ hv.invalidate();
+ }
+ }
+
+
+ @Override
+ protected boolean usePerfectFitBitmap() {
+ return false;
+ }
+
+ @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();
+ }
+ }
+
+ 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, 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, false);
+ break;
+ }
+
+ return true;
+ }
+
+ 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);
+ }
+
+ 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 HighlightView get(int i) {
+ return mHighlightViews.get(i);
+ }
+
+ public int size() {
+ return mHighlightViews.size();
+ }
+
+ public void add(HighlightView hv) {
+ mHighlightViews.add(hv);
+ invalidate();
+ }
+ }
+
+ private void fillCanvas(int width, int height, Canvas c) {
+ Paint paint = new Paint();
+ paint.setColor(0x00000000); // pure alpha
+ paint.setStyle(android.graphics.Paint.Style.FILL);
+ paint.setAntiAlias(true);
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+ c.drawRect(0F, 0F, width, height, paint);
+ }
+
+ @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);
+
+ try {
+ android.content.Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ if (Config.LOGV)
+ Log.v(TAG, "extras are " + extras);
+ if (extras != null) {
+ for (String s: extras.keySet()) {
+ if (Config.LOGV)
+ Log.v(TAG, "" + s + " >>> " + extras.get(s));
+ }
+ if (extras.getString("circleCrop") != null) {
+ mCircleCrop = true;
+ mAspectX = 1;
+ mAspectY = 1;
+ }
+ mSaveUri = (Uri) extras.getParcelable("output");
+ if (mSaveUri != null) {
+ String compressFormatString = extras.getString("outputFormat");
+ if (compressFormatString != null)
+ mSaveFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
+ }
+ 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(target, CropImage.this, ImageManager.SORT_ASCENDING);
+ mImage = mAllImages.getImageForUri(target);
+
+ // don't read in really large bitmaps. max out at 1000.
+ // TODO when saving the resulting bitmap use the decode/crop/encode
+ // api so we don't lose any resolution
+ mBitmap = mImage.thumbBitmap();
+ if (Config.LOGV)
+ Log.v(TAG, "thumbBitmap returned " + mBitmap);
+ }
+
+ if (mBitmap == null) {
+ finish();
+ return;
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ mFaceDetectionDialog = ProgressDialog.show(CropImage.this,
+ null,
+ getResources().getString(R.string.runningFaceDetection),
+ true, false);
+ mImageView.setImageBitmapResetBase(mBitmap, true, true);
+ if (mImageView.getScale() == 1F)
+ mImageView.center(true, true, false);
+
+ new Thread(new Runnable() {
+ public void run() {
+ final Bitmap b = mImage != null ? mImage.fullSizeBitmap(500) : mBitmap;
+ if (Config.LOGV)
+ Log.v(TAG, "back from mImage.fullSizeBitmap(500) with bitmap of size " + b.getWidth() + " / " + b.getHeight());
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (b != mBitmap && b != null) {
+ mBitmap = b;
+ mImageView.setImageBitmapResetBase(b, true, false);
+ }
+ if (mImageView.getScale() == 1F)
+ mImageView.center(true, true, false);
+
+ new Thread(mRunFaceDetection).start();
+ }
+ });
+ }
+ }).start();
+ }}, 100);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to load bitmap", e);
+ finish();
+ }
+
+ findViewById(R.id.discard).setOnClickListener(new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ findViewById(R.id.save).setOnClickListener(new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ // 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
+ mSaving = true;
+ if (mCroppedImage == null) {
+ if (mCrop == null) {
+ if (Config.LOGV)
+ Log.v(TAG, "no cropped image...");
+ return;
+ }
+
+ Rect r = mCrop.getCropRect();
+
+ int width = r.width();
+ int height = r.height();
+
+ // if we're circle cropping we'll want alpha which is the third param here
+ mCroppedImage = Bitmap.createBitmap(width, height,
+ mCircleCrop ?
+ Bitmap.Config.ARGB_8888 :
+ Bitmap.Config.RGB_565);
+ Canvas c1 = new Canvas(mCroppedImage);
+ c1.drawBitmap(mBitmap, r, new Rect(0, 0, width, height), null);
+
+ 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 (mCroppedImage);
+ android.graphics.Path p = new android.graphics.Path();
+ p.addCircle(width/2F, height/2F, width/2F, android.graphics.Path.Direction.CW);
+ c.clipPath(p, Region.Op.DIFFERENCE);
+
+ fillCanvas(width, height, c);
+ }
+ }
+
+ /* If the output is required to a specific size then scale or fill */
+ if (mOutputX != 0 && mOutputY != 0) {
+
+ if (mScale) {
+
+ /* Scale the image to the required dimensions */
+ mCroppedImage = ImageLoader.transform(new Matrix(),
+ mCroppedImage, mOutputX, mOutputY, mScaleUp);
+ } else {
+
+ /* Don't scale the image crop it to the size requested.
+ * Create an new image with the cropped image in the center and
+ * the extra space filled.
+ */
+
+ /* Don't scale the image but instead fill it so it's the required dimension */
+ Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY, Bitmap.Config.RGB_565);
+ Canvas c1 = new Canvas(b);
+
+ /* Draw the cropped bitmap in the center */
+ Rect r = mCrop.getCropRect();
+ int left = (mOutputX / 2) - (r.width() / 2);
+ int top = (mOutputY / 2) - (r.width() / 2);
+ c1.drawBitmap(mBitmap, r, new Rect(left, top, left
+ + r.width(), top + r.height()), null);
+
+ /* Set the cropped bitmap as the new bitmap */
+ mCroppedImage = b;
+ }
+ }
+
+ Bundle myExtras = getIntent().getExtras();
+ if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean("return-data"))) {
+ Bundle extras = new Bundle();
+ extras.putParcelable("data", mCroppedImage);
+ setResult(RESULT_OK,
+ (new Intent()).setAction("inline-data").putExtras(extras));
+ finish();
+ } else {
+ if (!isFinishing()) {
+ mSavingProgressDialog = ProgressDialog.show(CropImage.this,
+ null,
+ getResources().getString(R.string.savingImage),
+ true, true);
+ }
+ Runnable r = new Runnable() {
+ public void run() {
+ if (mSaveUri != null) {
+ OutputStream outputStream = null;
+ try {
+ String scheme = mSaveUri.getScheme();
+ if (scheme.equals("file")) {
+ outputStream = new FileOutputStream(mSaveUri.toString().substring(scheme.length()+":/".length()));
+ } else {
+ outputStream = mContentResolver.openOutputStream(mSaveUri);
+ }
+ if (outputStream != null)
+ mCroppedImage.compress(mSaveFormat, 75, outputStream);
+
+ } catch (IOException ex) {
+ if (Config.LOGV)
+ Log.v(TAG, "got IOException " + ex);
+ } finally {
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+ Bundle extras = new Bundle();
+ setResult(RESULT_OK,
+ (new Intent())
+ .setAction(mSaveUri.toString())
+ .putExtras(extras));
+ } else {
+ Bundle extras = new Bundle();
+ extras.putString("rect", mCrop.getCropRect().toString());
+
+ // here we decide whether to create a new image or
+ // modify the existing image
+ if (false) {
+ /*
+ // this is the "modify" case
+ ImageManager.IGetBoolean_cancelable cancelable =
+ mImage.saveImageContents(mCroppedImage, null, null, null, mImage.getDateTaken(), 0, false);
+ boolean didSave = cancelable.get();
+ extras.putString("thumb1uri", mImage.thumbUri().toString());
+ setResult(RESULT_OK,
+ (new Intent()).setAction(mImage.fullSizeImageUri().toString())
+ .putExtras(extras));
+ */
+ } else {
+ // this is the "new image" case
+ java.io.File oldPath = new java.io.File(mImage.getDataPath());
+ java.io.File directory = new java.io.File(oldPath.getParent());
+
+ int x = 0;
+ String fileName = oldPath.getName();
+ fileName = fileName.substring(0, fileName.lastIndexOf("."));
+
+ while (true) {
+ x += 1;
+ String candidate = directory.toString() + "/" + fileName + "-" + x + ".jpg";
+ if (Config.LOGV)
+ Log.v(TAG, "candidate is " + candidate);
+ boolean exists = (new java.io.File(candidate)).exists();
+ if (!exists)
+ break;
+ }
+
+ try {
+ Uri newUri = ImageManager.instance().addImage(
+ CropImage.this,
+ getContentResolver(),
+ mImage.getTitle(),
+ mImage.getDescription(),
+ mImage.getDateTaken(),
+ null, // TODO this null is going to cause us to lose the location (gps)
+ 0, // TODO this is going to cause the orientation to reset
+ directory.toString(),
+ fileName + "-" + x + ".jpg");
+
+ ImageManager.IAddImage_cancelable cancelable = ImageManager.instance().storeImage(
+ newUri,
+ CropImage.this,
+ getContentResolver(),
+ 0, // TODO fix this orientation
+ mCroppedImage,
+ null);
+
+ cancelable.get();
+ setResult(RESULT_OK,
+ (new Intent()).setAction(newUri.toString())
+ .putExtras(extras));
+ } catch (Exception ex) {
+ // basically ignore this or put up
+ // some ui saying we failed
+ }
+ }
+ }
+ finish();
+ }
+ };
+ Thread t = new Thread(r);
+ t.start();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ }
+
+ Handler mHandler = new Handler();
+
+ Runnable mRunFaceDetection = new Runnable() {
+ float mScale = 1F;
+ RectF mUnion = null;
+ Matrix mImageMatrix;
+ FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
+ int mNumFaces;
+
+ 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 = makeHighlightView();
+
+ 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);
+
+ if (mUnion == null) {
+ mUnion = new RectF(faceRect);
+ } else {
+ mUnion.union(faceRect);
+ }
+
+ mImageView.add(hv);
+ }
+
+ private HighlightView makeHighlightView() {
+ return new HighlightView(mImageView);
+ }
+
+ private void makeDefault() {
+ HighlightView hv = makeHighlightView();
+
+ 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;
+// Log.v(TAG, "adjusted cropHeight to " + cropHeight);
+ } else {
+ cropWidth = cropHeight * mAspectX / mAspectY;
+// Log.v(TAG, "adjusted cropWidth to " + cropWidth);
+ }
+ }
+
+ 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);
+ }
+
+ private Bitmap prepareBitmap() {
+ if (mBitmap == null)
+ return null;
+
+ // scale the image down for faster face detection
+ // 256 pixels wide is enough.
+ if (mBitmap.getWidth() > 256) {
+ mScale = 256.0F / (float) 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 (Config.LOGV)
+ Log.v(TAG, "numFaces is " + mNumFaces);
+ }
+ 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);
+ }
+
+ closeProgressDialog();
+
+ if (mNumFaces > 1) {
+ Toast t = Toast.makeText(CropImage.this, R.string.multiface_crop_help, Toast.LENGTH_SHORT);
+ t.show();
+ }
+ }
+ });
+
+ }
+ };
+
+ @Override
+ public void onStop() {
+ closeProgressDialog();
+ super.onStop();
+ if (mAllImages != null)
+ mAllImages.deactivate();
+ }
+
+ private synchronized void closeProgressDialog() {
+ if (mFaceDetectionDialog != null) {
+ mFaceDetectionDialog.dismiss();
+ mFaceDetectionDialog = null;
+ }
+ if (mSavingProgressDialog != null) {
+ mSavingProgressDialog.dismiss();
+ mSavingProgressDialog = null;
+ }
+ }
+}
diff --git a/src/com/android/camera/DrmWallpaper.java b/src/com/android/camera/DrmWallpaper.java
new file mode 100644
index 0000000..10f33dc
--- /dev/null
+++ b/src/com/android/camera/DrmWallpaper.java
@@ -0,0 +1,35 @@
+/*
+ * 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 android.content.Intent;
+import android.content.pm.PackageManager;
+import android.util.Log;
+
+
+/**
+ * Wallpaper picker for DRM images. This just redirects to the standard pick action.
+ */
+public class DrmWallpaper extends Wallpaper {
+
+ protected void formatIntent(Intent intent) {
+ super.formatIntent(intent);
+ intent.putExtra("pick-drm", true);
+ }
+
+}
diff --git a/src/com/android/camera/ErrorScreen.java b/src/com/android/camera/ErrorScreen.java
new file mode 100644
index 0000000..1018eb7
--- /dev/null
+++ b/src/com/android/camera/ErrorScreen.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2006 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.Intent;
+import android.app.Activity;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.Bundle;
+import android.widget.TextView;
+
+
+/**
+ *
+ */
+public class ErrorScreen extends Activity
+{
+ int mError;
+ boolean mLogoutOnExit;
+ boolean mReconnectOnExit;
+ Handler mHandler = new Handler();
+
+ Runnable mCloseScreenCallback = new Runnable() {
+ public void run() {
+ finish();
+ }
+ };
+
+ @Override public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resolveIntent();
+
+ String errMsg = null;
+
+ // PENDING: resourcify error messages!
+
+ switch (mError) {
+ default:
+ errMsg = "You need to setup your Picassa Web account first.";
+ break;
+ }
+
+ TextView tv = new TextView(this);
+ tv.setText(errMsg);
+ setContentView(tv);
+ }
+
+ @Override public void onResume() {
+ super.onResume();
+
+ mHandler.postAtTime(mCloseScreenCallback,
+ SystemClock.uptimeMillis() + 5000);
+
+ }
+
+ @Override public void onStop() {
+ super.onStop();
+ mHandler.removeCallbacks(mCloseScreenCallback);
+// startNextActivity();
+ }
+
+ void resolveIntent() {
+ Intent intent = getIntent();
+ mError = intent.getIntExtra("error", mError);
+
+ mLogoutOnExit = intent.getBooleanExtra("logout", mLogoutOnExit);
+ mReconnectOnExit = intent.getBooleanExtra("reconnect", mReconnectOnExit);
+ }
+
+// void startNextActivity() {
+// GTalkApp app = GTalkApp.getInstance();
+//
+// if (mLogoutOnExit) {
+// app.logout();
+// }
+// else if (mReconnectOnExit) {
+// app.showLogin(false);
+// }
+// }
+}
diff --git a/src/com/android/camera/ExifInterface.java b/src/com/android/camera/ExifInterface.java
new file mode 100644
index 0000000..de2fd5c
--- /dev/null
+++ b/src/com/android/camera/ExifInterface.java
@@ -0,0 +1,263 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Iterator;
+
+import android.util.Config;
+import android.util.Log;
+
+// Wrapper for native Exif library
+
+public class ExifInterface {
+
+ private String mFilename;
+
+ // Constants used for the Orientation Exif tag.
+ static final int ORIENTATION_UNDEFINED = 0;
+ static final int ORIENTATION_NORMAL = 1;
+ static final int ORIENTATION_FLIP_HORIZONTAL = 2; // left right reversed mirror
+ static final int ORIENTATION_ROTATE_180 = 3;
+ static final int ORIENTATION_FLIP_VERTICAL = 4; // upside down mirror
+ static final int ORIENTATION_TRANSPOSE = 5; // flipped about top-left <--> bottom-right axis
+ static final int ORIENTATION_ROTATE_90 = 6; // rotate 90 cw to right it
+ static final int ORIENTATION_TRANSVERSE = 7; // flipped about top-right <--> bottom-left axis
+ static final int ORIENTATION_ROTATE_270 = 8; // rotate 270 to right it
+
+ // The Exif tag names
+ static final String TAG_ORIENTATION = "Orientation";
+ static final String TAG_DATE_TIME_ORIGINAL = "DateTimeOriginal";
+ static final String TAG_MAKE = "Make";
+ static final String TAG_MODEL = "Model";
+ static final String TAG_FLASH = "Flash";
+ static final String TAG_IMAGE_WIDTH = "ImageWidth";
+ static final String TAG_IMAGE_LENGTH = "ImageLength";
+
+ static final String TAG_GPS_LATITUDE = "GPSLatitude";
+ static final String TAG_GPS_LONGITUDE = "GPSLongitude";
+
+ static final String TAG_GPS_LATITUDE_REF = "GPSLatitudeRef";
+ static final String TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef";
+
+ private boolean mSavedAttributes = false;
+ private boolean mHasThumbnail = false;
+ private HashMap<String, String> mCachedAttributes = null;
+
+ static {
+ System.loadLibrary("exif");
+ }
+
+ public ExifInterface(String fileName) {
+ mFilename = fileName;
+ }
+
+ /**
+ * Given a HashMap of Exif tags and associated values, an Exif section in the JPG file
+ * is created and loaded with the tag data. saveAttributes() is expensive because it involves
+ * copying all the JPG data from one file to another and deleting the old file and renaming the other.
+ * It's best to collect all the attributes to write and make a single call rather than multiple
+ * calls for each attribute. You must call "commitChanges()" at some point to commit the changes.
+ */
+ public void saveAttributes(HashMap<String, String> attributes) {
+ // format of string passed to native C code:
+ // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
+ // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
+ StringBuilder sb = new StringBuilder();
+ int size = attributes.size();
+ if (attributes.containsKey("hasThumbnail")) {
+ --size;
+ }
+ sb.append(size + " ");
+ Iterator keyIterator = attributes.keySet().iterator();
+ while (keyIterator.hasNext()) {
+ String key = (String)keyIterator.next();
+ if (key.equals("hasThumbnail")) {
+ continue; // this is a fake attribute not saved as an exif tag
+ }
+ String val = (String)attributes.get(key);
+ sb.append(key + "=");
+ sb.append(val.length() + " ");
+ sb.append(val);
+ }
+ String s = sb.toString();
+ if (android.util.Config.LOGV)
+ android.util.Log.v("camera", "saving exif data: " + s);
+ saveAttributesNative(mFilename, s);
+ mSavedAttributes = true;
+ }
+
+ /**
+ * Returns a HashMap loaded with the Exif attributes of the file. The key is the standard
+ * tag name and the value is the tag's value: e.g. Model -> Nikon. Numeric values are
+ * returned as strings.
+ */
+ public HashMap<String, String> getAttributes() {
+ if (mCachedAttributes != null) {
+ return mCachedAttributes;
+ }
+ // format of string passed from native C code:
+ // "attrCnt attr1=valueLen value1attr2=value2Len value2..."
+ // example: "4 attrPtr ImageLength=4 1024Model=6 FooImageWidth=4 1280Make=3 FOO"
+ mCachedAttributes = new HashMap<String, String>();
+
+ String attrStr = getAttributesNative(mFilename);
+
+ // get count
+ int ptr = attrStr.indexOf(' ');
+ int count = Integer.parseInt(attrStr.substring(0, ptr));
+ ++ptr; // skip past the space between item count and the rest of the attributes
+
+ for (int i = 0; i < count; i++) {
+ // extract the attribute name
+ int equalPos = attrStr.indexOf('=', ptr);
+ String attrName = attrStr.substring(ptr, equalPos);
+ ptr = equalPos + 1; // skip past =
+
+ // extract the attribute value length
+ int lenPos = attrStr.indexOf(' ', ptr);
+ int attrLen = Integer.parseInt(attrStr.substring(ptr, lenPos));
+ ptr = lenPos + 1; // skip pas the space
+
+ // extract the attribute value
+ String attrValue = attrStr.substring(ptr, ptr + attrLen);
+ ptr += attrLen;
+
+ if (attrName.equals("hasThumbnail")) {
+ mHasThumbnail = attrValue.equalsIgnoreCase("true");
+ } else {
+ mCachedAttributes.put(attrName, attrValue);
+ }
+ }
+ return mCachedAttributes;
+ }
+
+ /**
+ * Given a numerical orientation, return a human-readable string describing the orientation.
+ */
+ static public String orientationToString(int orientation) {
+ // TODO: this function needs to be localized and use string resource ids rather than strings
+ String orientationString;
+ switch (orientation) {
+ case ORIENTATION_NORMAL: orientationString = "Normal"; break;
+ case ORIENTATION_FLIP_HORIZONTAL: orientationString = "Flipped horizontal"; break;
+ case ORIENTATION_ROTATE_180: orientationString = "Rotated 180 degrees"; break;
+ case ORIENTATION_FLIP_VERTICAL: orientationString = "Upside down mirror"; break;
+ case ORIENTATION_TRANSPOSE: orientationString = "Transposed"; break;
+ case ORIENTATION_ROTATE_90: orientationString = "Rotated 90 degrees"; break;
+ case ORIENTATION_TRANSVERSE: orientationString = "Transversed"; break;
+ case ORIENTATION_ROTATE_270: orientationString = "Rotated 270 degrees"; break;
+ default: orientationString = "Undefined"; break;
+ }
+ return orientationString;
+ }
+
+ /**
+ * Copies the thumbnail data out of the filename and puts it in the Exif data associated
+ * with the file used to create this object. You must call "commitChanges()" at some point
+ * to commit the changes.
+ */
+ public boolean appendThumbnail(String thumbnailFileName) {
+ if (!mSavedAttributes) {
+ throw new RuntimeException("Must call saveAttributes before calling appendThumbnail");
+ }
+ mHasThumbnail = appendThumbnailNative(mFilename, thumbnailFileName);
+ return mHasThumbnail;
+ }
+
+ /**
+ * Saves the changes (added Exif tags, added thumbnail) to the JPG file. You have to call
+ * saveAttributes() before committing the changes.
+ */
+ public void commitChanges() {
+ if (!mSavedAttributes) {
+ throw new RuntimeException("Must call saveAttributes before calling commitChanges");
+ }
+ commitChangesNative(mFilename);
+ }
+
+ public boolean hasThumbnail() {
+ if (!mSavedAttributes) {
+ getAttributes();
+ }
+ return mHasThumbnail;
+ }
+
+ public byte[] getThumbnail() {
+ return getThumbnailNative(mFilename);
+ }
+
+ static public String convertRationalLatLonToDecimalString(String rationalString, String ref, boolean usePositiveNegative) {
+ try {
+ String [] parts = rationalString.split(",");
+
+ String [] pair;
+ pair = parts[0].split("/");
+ int degrees = (int) (Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim()));
+
+ pair = parts[1].split("/");
+ int minutes = (int) ((Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim())));
+
+ pair = parts[2].split("/");
+ float seconds = Float.parseFloat(pair[0].trim()) / Float.parseFloat(pair[1].trim());
+
+ float result = degrees + (minutes/60F) + (seconds/(60F*60F));
+
+ String preliminaryResult = String.valueOf(result);
+ if (usePositiveNegative) {
+ String neg = (ref.equals("S") || ref.equals("E")) ? "-" : "";
+ return neg + preliminaryResult;
+ } else {
+ return preliminaryResult + String.valueOf((char)186) + " " + ref;
+ }
+ } catch (Exception ex) {
+ // if for whatever reason we can't parse the lat long then return null
+ return null;
+ }
+ }
+
+ static public String makeLatLongString(double d) {
+ d = Math.abs(d);
+
+ int degrees = (int) d;
+
+ double remainder = d - (double)degrees;
+ int minutes = (int) (remainder * 60D);
+ int seconds = (int) (((remainder * 60D) - minutes) * 60D * 1000D); // really seconds * 1000
+
+ String retVal = degrees + "/1," + minutes + "/1," + (int)seconds + "/1000";
+ return retVal;
+ }
+
+ static public String makeLatStringRef(double lat) {
+ return lat >= 0D ? "N" : "S";
+ }
+
+ static public String makeLonStringRef(double lon) {
+ return lon >= 0D ? "W" : "E";
+ }
+
+ private native boolean appendThumbnailNative(String fileName, String thumbnailFileName);
+
+ private native void saveAttributesNative(String fileName, String compressedAttributes);
+
+ private native String getAttributesNative(String fileName);
+
+ private native void commitChangesNative(String fileName);
+
+ private native byte[] getThumbnailNative(String fileName);
+}
diff --git a/src/com/android/camera/GalleryPicker.java b/src/com/android/camera/GalleryPicker.java
new file mode 100644
index 0000000..9f1664b
--- /dev/null
+++ b/src/com/android/camera/GalleryPicker.java
@@ -0,0 +1,607 @@
+/*
+ * 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 android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.StatFs;
+import android.preference.PreferenceManager;
+import android.provider.MediaStore.Images;
+import android.util.Config;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class GalleryPicker extends Activity {
+ static private final String TAG = "GalleryPicker";
+
+ GridView mGridView;
+ Drawable mFrameGalleryMask;
+ Drawable mCellOutline;
+
+ BroadcastReceiver mReceiver;
+ GalleryPickerAdapter mAdapter;
+
+ Dialog mMediaScanningDialog;
+
+ MenuItem mFlipItem;
+ SharedPreferences mPrefs;
+
+ boolean mPausing = false;
+
+ private static long LOW_STORAGE_THRESHOLD = 1024 * 1024 * 2;
+
+ public GalleryPicker() {
+ }
+
+ private void rebake(boolean unmounted, boolean scanning) {
+ if (mMediaScanningDialog != null) {
+ mMediaScanningDialog.cancel();
+ mMediaScanningDialog = null;
+ }
+ if (scanning) {
+ mMediaScanningDialog = ProgressDialog.show(
+ this,
+ null,
+ getResources().getString(R.string.wait),
+ true,
+ true);
+ }
+ mAdapter.notifyDataSetChanged();
+ mAdapter.init(!unmounted && !scanning);
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ setContentView(R.layout.gallerypicker);
+
+ mGridView = (GridView) findViewById(R.id.albums);
+ mGridView.setSelector(android.R.color.transparent);
+
+ mReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Config.LOGV) Log.v(TAG, "onReceiveIntent " + intent.getAction());
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ if (Config.LOGV) Log.v(TAG, "sd card no longer available");
+ Toast.makeText(GalleryPicker.this, getResources().getString(R.string.wait), 5000);
+ rebake(true, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(GalleryPicker.this, getResources().getString(R.string.wait), 5000);
+ rebake(false, true);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_SCANNER_FINISHED");
+ rebake(false, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_EJECT");
+ rebake(true, false);
+ }
+ }
+ };
+
+ mGridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ launchFolderGallery(position);
+ }
+ });
+ mGridView.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, final ContextMenu.ContextMenuInfo menuInfo) {
+ menu.setHeaderTitle(
+ mAdapter.baseTitleForPosition(((AdapterContextMenuInfo)menuInfo).position));
+ menu.add(0, 207, 0, R.string.slide_show)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
+ int position = info.position;
+
+ Uri targetUri;
+ synchronized (mAdapter.mFirstImageUris) {
+ if (position >= mAdapter.mFirstImageUris.size()) {
+ // the list of ids does not include the "all" list
+ targetUri = mAdapter.firstImageUri(mAdapter.mIds.get(position-1));
+ } else {
+ // the mFirstImageUris list includes the "all" uri
+ targetUri = mAdapter.mFirstImageUris.get(position);
+ }
+ }
+ if (targetUri != null && position > 0) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", mAdapter.mIds.get(info.position-1)).build();
+ }
+// Log.v(TAG, "URI to launch slideshow " + targetUri);
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+ intent.putExtra("slideshow", true);
+ startActivity(intent);
+ return true;
+ }
+ });
+ menu.add(0, 208, 0, R.string.view)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
+ launchFolderGallery(info.position);
+ return true;
+ }
+ });
+ }
+ });
+ }
+
+ private void launchFolderGallery(int position) {
+ android.net.Uri uri = Images.Media.INTERNAL_CONTENT_URI;
+ if (position > 0) {
+ uri = uri.buildUpon().appendQueryParameter("bucketId", mAdapter.mIds.get(position-1)).build();
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ if (position > 0) {
+ intent.putExtra("windowTitle", mAdapter.mNames.get(position-1));
+ }
+ startActivity(intent);
+ }
+
+ class ItemInfo {
+ Bitmap bitmap;
+ int count;
+ int overlayId;
+ }
+
+ class GalleryPickerAdapter extends BaseAdapter {
+ ArrayList<String> mIds = new ArrayList<String>();
+ ArrayList<String> mNames = new ArrayList<String>();
+ ArrayList<Uri> mFirstImageUris = new ArrayList<Uri>();
+
+ ArrayList<View> mAllViews = new ArrayList<View>();
+ SparseArray<ItemInfo> mThumbs = new SparseArray<ItemInfo>();
+
+ boolean mDone = false;
+ CameraThread mWorkerThread;
+
+ public void init(boolean assumeMounted) {
+ mAllViews.clear();
+ mThumbs.clear();
+
+ ImageManager.IImageList images;
+ if (assumeMounted) {
+ images = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING);
+ } else {
+ images = ImageManager.instance().emptyImageList();
+ }
+
+ mIds.clear();
+ mNames.clear();
+ mFirstImageUris.clear();
+
+ if (mWorkerThread != null) {
+ try {
+ mDone = true;
+ if (Config.LOGV)
+ Log.v(TAG, "about to call join on thread " + mWorkerThread.getId());
+ mWorkerThread.join();
+ } finally {
+ mWorkerThread = null;
+ }
+ }
+
+ String cameraItem = ImageManager.CAMERA_IMAGE_BUCKET_ID;
+ final HashMap<String, String> hashMap = images.getBucketIds();
+ String cameraBucketId = null;
+ for (String key : hashMap.keySet()) {
+ if (key.equals(cameraItem)) {
+ cameraBucketId = key;
+ } else {
+ mIds.add(key);
+ }
+ }
+ images.deactivate();
+ notifyDataSetInvalidated();
+
+ // sort baesd on the display name. if two display names compare equal
+ // then sort based on the id
+ java.util.Collections.sort(mIds, new java.util.Comparator<String>() {
+ public int compare(String first, String second) {
+ int x = hashMap.get(first).compareTo(hashMap.get(second));
+ if (x == 0)
+ x = first.compareTo(second);
+ return x;
+ }
+ });
+
+ for (String s : mIds) {
+ mNames.add(hashMap.get(s));
+ }
+
+ if (cameraBucketId != null) {
+ mIds.add(0, cameraBucketId);
+ mNames.add(0, "Camera");
+ }
+ final boolean foundCameraBucket = cameraBucketId != null;
+
+ mDone = false;
+ mWorkerThread = new CameraThread(new Runnable() {
+ public void run() {
+ try {
+ // no images, nothing to do
+ if (mIds.size() == 0)
+ return;
+
+ for (int i = 0; i < mIds.size() + 1 && !mDone; i++) {
+ String id = i == 0 ? null : mIds.get(i-1);
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING,
+ id);
+ try {
+ if (mPausing) {
+ break;
+ }
+ if (list.getCount() > 0)
+ mFirstImageUris.add(i, list.getImageAt(0).fullSizeImageUri());
+
+ int overlay = -1;
+ if (i == 1 && foundCameraBucket)
+ overlay = R.drawable.frame_overlay_gallery_camera;
+ final Bitmap b = makeMiniThumbBitmap(142, 142, list);
+ final int pos = i;
+ final int count = list.getCount();
+ final int overlayId = overlay;
+ final Thread currentThread = Thread.currentThread();
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mPausing || currentThread != mWorkerThread.realThread()) {
+ if (b != null) {
+ b.recycle();
+ }
+ return;
+ }
+
+ ItemInfo info = new ItemInfo();
+ info.bitmap = b;
+ info.count = count;
+ info.overlayId = overlayId;
+ mThumbs.put(pos, info);
+
+ final GridView grid = GalleryPicker.this.mGridView;
+ final int firstVisible = grid.getFirstVisiblePosition();
+
+ // Minor optimization -- only notify if the specified position is visible
+ if ((pos >= firstVisible) && (pos < firstVisible + grid.getChildCount())) {
+ GalleryPickerAdapter.this.notifyDataSetChanged();
+ }
+ }
+ });
+ } finally {
+ list.deactivate();
+ }
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception generating collage views " + ex.toString());
+ }
+ }
+ });
+ mWorkerThread.start();
+ mWorkerThread.toBackground();
+ }
+
+ Uri firstImageUri(String id) {
+ ImageManager.IImageList list = ImageManager.instance().allImages(
+ GalleryPicker.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ ImageManager.SORT_DESCENDING,
+ id);
+ Uri uri = list.getImageAt(0).fullSizeImageUri();
+ list.deactivate();
+ return uri;
+ }
+
+ public int getCount() {
+ return mIds.size() + 1; // add 1 for the everything bucket
+ }
+
+ public Object getItem(int position) {
+ return null;
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ private String baseTitleForPosition(int position) {
+ if (position == 0) {
+ return getResources().getString(R.string.all_images);
+ } else {
+ return mNames.get(position-1);
+ }
+ }
+
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v;
+
+ if (convertView == null) {
+ LayoutInflater vi = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ v = vi.inflate(R.layout.gallery_picker_item, null);
+ } else {
+ v = convertView;
+ }
+
+ TextView titleView = (TextView) v.findViewById(R.id.title);
+
+
+ GalleryPickerItem iv = (GalleryPickerItem) v.findViewById(R.id.thumbnail);
+ ItemInfo info = mThumbs.get(position);
+ if (info != null) {
+ iv.setImageBitmap(info.bitmap);
+ iv.setOverlay(info.overlayId);
+ String title = baseTitleForPosition(position) + " (" + info.count + ")";
+ titleView.setText(title);
+ } else {
+ iv.setImageResource(android.R.color.transparent);
+ iv.setOverlay(-1);
+ titleView.setText(baseTitleForPosition(position));
+ }
+
+ return v;
+ }
+ };
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPausing = true;
+ unregisterReceiver(mReceiver);
+
+ // free up some ram
+ mAdapter = null;
+ mGridView.setAdapter(null);
+ System.gc();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mPausing = false;
+
+ mAdapter = new GalleryPickerAdapter();
+ mGridView.setAdapter(mAdapter);
+ setBackgrounds(getResources());
+
+ boolean scanning = ImageManager.isMediaScannerScanning(this);
+ rebake(false, scanning);
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addDataScheme("file");
+
+ registerReceiver(mReceiver, intentFilter);
+ MenuHelper.requestOrientation(this, mPrefs);
+
+ // Warn the user if space is getting low
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+
+ // Check available space only if we are writable
+ if (ImageManager.hasStorage()) {
+ String storageDirectory = Environment.getExternalStorageDirectory().toString();
+ StatFs stat = new StatFs(storageDirectory);
+ long remaining = (long)stat.getAvailableBlocks() * (long)stat.getBlockSize();
+ if (remaining < LOW_STORAGE_THRESHOLD) {
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ Toast.makeText(GalleryPicker.this.getApplicationContext(),
+ R.string.not_enough_space, 5000).show();
+ }
+ });
+ }
+ }
+ }
+ });
+ t.start();
+
+ if (!scanning && mAdapter.mIds.size() <= 1) {
+ android.net.Uri uri = Images.Media.INTERNAL_CONTENT_URI;
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ startActivity(intent);
+ finish();
+ return;
+ }
+ }
+
+
+ private void setBackgrounds(Resources r) {
+ mFrameGalleryMask = r.getDrawable(R.drawable.frame_gallery_preview_album_mask);
+
+ mCellOutline = r.getDrawable(android.R.drawable.gallery_thumb);
+ }
+
+ Handler mHandler = new Handler();
+
+ private void placeImage(Bitmap image, Canvas c, Paint paint, int imageWidth, int widthPadding, int imageHeight, int heightPadding, int offsetX, int offsetY, int pos) {
+ int row = pos / 2;
+ int col = pos - (row * 2);
+
+ int xPos = (col * (imageWidth + widthPadding)) - offsetX;
+ int yPos = (row * (imageHeight + heightPadding)) - offsetY;
+
+ c.drawBitmap(image, xPos, yPos, paint);
+ }
+
+ private Bitmap makeMiniThumbBitmap(int width, int height, ImageManager.IImageList images) {
+ int count = images.getCount();
+ // We draw three different version of the folder image depending on the number of images in the folder.
+ // For a single image, that image draws over the whole folder.
+ // For two or three images, we draw the two most recent photos.
+ // For four or more images, we draw four photos.
+ final int padding = 4;
+ int imageWidth = width;
+ int imageHeight = height;
+ int offsetWidth = 0;
+ int offsetHeight = 0;
+ if (count < 4) {
+ count = 1;
+ // uncomment for 2 pictures per frame
+// if (count == 2 || count == 3) {
+// count = 2;
+// imageWidth = imageWidth * 2 / 3;
+// imageHeight = imageHeight * 2 / 3;
+// offsetWidth = imageWidth / 3 - padding;
+// offsetHeight = -imageHeight / 3 + padding * 2;
+ } else if (count >= 4) {
+ count = 4;
+ imageWidth = (imageWidth - padding) / 2; // 2 here because we show two images
+ imageHeight = (imageHeight - padding) / 2; // per row and column
+ }
+ final Paint p = new Paint();
+ final Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas c = new Canvas(b);
+
+ final Matrix m = new Matrix();
+
+ // draw the whole canvas as transparent
+ p.setColor(0x00000000);
+ c.drawPaint(p);
+
+ // draw the mask normally
+ p.setColor(0xFFFFFFFF);
+ mFrameGalleryMask.setBounds(0, 0, width, height);
+ mFrameGalleryMask.draw(c);
+
+ Paint pdpaint = new Paint();
+ pdpaint.setXfermode(new android.graphics.PorterDuffXfermode(
+ android.graphics.PorterDuff.Mode.SRC_IN));
+
+ pdpaint.setStyle(Paint.Style.FILL);
+ c.drawRect(0, 0, width, height, pdpaint);
+
+ for (int i = 0; i < count; i++) {
+ if (mPausing) {
+ return null;
+ }
+ ImageManager.IImage image = i < count ? images.getImageAt(i) : null;
+ if (image == null) {
+ break;
+ }
+ Bitmap temp = image.miniThumbBitmap();
+ if (temp != null) {
+ Bitmap temp2 = ImageLoader.transform(m, temp, imageWidth, imageHeight, true);
+ if (temp2 != temp)
+ temp.recycle();
+ temp = temp2;
+ }
+
+ Bitmap thumb = Bitmap.createBitmap(imageWidth, imageHeight, Bitmap.Config.ARGB_8888);
+ Canvas tempCanvas = new Canvas(thumb);
+ if (temp != null)
+ tempCanvas.drawBitmap(temp, new Matrix(), new Paint());
+ mCellOutline.setBounds(0, 0, imageWidth, imageHeight);
+ mCellOutline.draw(tempCanvas);
+
+ placeImage(thumb, c, pdpaint, imageWidth, padding, imageHeight, padding, offsetWidth, offsetHeight, i);
+
+ thumb.recycle();
+
+ if (temp != null)
+ temp.recycle();
+ }
+ return b;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, this, mPrefs);
+
+ menu.add(0, 0, 0, R.string.camerasettings)
+ .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(GalleryPicker.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('p')
+ .setIcon(android.R.drawable.ic_menu_preferences);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(android.view.Menu menu) {
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ return true;
+ }
+}
diff --git a/src/com/android/camera/GalleryPickerItem.java b/src/com/android/camera/GalleryPickerItem.java
new file mode 100644
index 0000000..3fc9678
--- /dev/null
+++ b/src/com/android/camera/GalleryPickerItem.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2008 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.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+public class GalleryPickerItem extends ImageView {
+
+ private Drawable mFrame;
+ private Rect mFrameBounds = new Rect();
+ private Drawable mOverlay;
+
+ public GalleryPickerItem(Context context) {
+ this(context, null);
+ }
+
+ public GalleryPickerItem(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public GalleryPickerItem(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mFrame = getResources().getDrawable(R.drawable.frame_gallery_preview);
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || (who == mFrame) || (who == mOverlay);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mFrame != null) {
+ int[] drawableState = getDrawableState();
+ mFrame.setState(drawableState);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ final Rect frameBounds = mFrameBounds;
+ if (frameBounds.isEmpty()) {
+ final int w = getWidth();
+ final int h = getHeight();
+
+ frameBounds.set(0, 0, w, h);
+ mFrame.setBounds(frameBounds);
+ if (mOverlay != null) {
+ mOverlay.setBounds(w - mOverlay.getIntrinsicWidth(),
+ h - mOverlay.getIntrinsicHeight(), w, h);
+ }
+ }
+
+ mFrame.draw(canvas);
+ if (mOverlay != null) {
+ mOverlay.draw(canvas);
+ }
+ }
+
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mFrameBounds.setEmpty();
+ }
+
+ public void setOverlay(int overlayId) {
+ if (overlayId >= 0) {
+ mOverlay = getResources().getDrawable(overlayId);
+ mFrameBounds.setEmpty();
+ } else {
+ mOverlay = null;
+ }
+ }
+}
diff --git a/src/com/android/camera/GallerySettings.java b/src/com/android/camera/GallerySettings.java
new file mode 100644
index 0000000..3af6867
--- /dev/null
+++ b/src/com/android/camera/GallerySettings.java
@@ -0,0 +1,46 @@
+/*
+ * 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 android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.content.Context;
+
+/**
+ * GallerySettings
+ */
+class GallerySettings extends CameraSettings
+{
+ public GallerySettings()
+ {
+ }
+
+ @Override
+ protected int resourceId() {
+ return R.xml.gallery_preferences;
+ }
+
+ /** Called with the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ }
+}
+
diff --git a/src/com/android/camera/HighlightView.java b/src/com/android/camera/HighlightView.java
new file mode 100644
index 0000000..594bab6
--- /dev/null
+++ b/src/com/android/camera/HighlightView.java
@@ -0,0 +1,428 @@
+/*
+ * 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 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.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+
+
+public class HighlightView
+{
+ private static final String TAG = "CropImage";
+ View mContext;
+ Path mPath;
+ Rect mViewDrawingRect = new Rect();
+
+ int mMotionMode;
+
+ 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)
+ {
+ super();
+ mContext = ctx;
+ mPath = new Path();
+ }
+
+ private void initHighlightView() {
+ 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();
+ mPath.reset();
+ if (!hasFocus()) {
+ mOutlinePaint.setColor(0xFF000000);
+ canvas.drawRect(mDrawRect, mOutlinePaint);
+ } else {
+ mContext.getDrawingRect(mViewDrawingRect);
+ if (mCircle) {
+ float width = mDrawRect.width() - (getPaddingLeft() + getPaddingRight());
+ float height = mDrawRect.height() - (getPaddingTop() + getPaddingBottom());
+ mPath.addCircle(
+ mDrawRect.left + getPaddingLeft() + (width / 2),
+ mDrawRect.top + getPaddingTop() + (height / 2),
+ width / 2,
+ Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFEF04D6);
+ } else {
+ mPath.addRect(new RectF(mDrawRect), Path.Direction.CW);
+ mOutlinePaint.setColor(0xFFFF8A00);
+ }
+ canvas.clipPath(mPath, Region.Op.DIFFERENCE);
+ canvas.drawRect(mViewDrawingRect, hasFocus() ? mFocusPaint : mNoFocusPaint);
+
+ canvas.restore();
+ canvas.drawPath(mPath, 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);
+ }
+ }
+ }
+
+ }
+
+ float getPaddingTop() { return 0F; }
+ float getPaddingBottom() { return 0F; }
+ float getPaddingLeft() { return 0F; }
+ float getPaddingRight() { return 0F; }
+
+ public ModifyMode getMode() {
+ return mMode;
+ }
+
+ public void setMode(ModifyMode mode)
+ {
+ if (mode != mMode) {
+ mMode = mode;
+ mContext.invalidate();
+ }
+ }
+
+ 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 = (int) (mDrawRect.width() - getPaddingLeft()) / 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;
+ }
+// Log.v(TAG, "radius: " + radius + "; touchRadius: " + distanceFromCenter + "; distX: " + distX + "; distY: " + distY + "; retval: " + retval);
+ } else {
+ boolean verticalCheck = (y >= r.top - hysteresis) && (y < r.bottom + hysteresis);
+ boolean horizCheck = (x >= r.left - hysteresis) && (x < r.right + hysteresis);
+
+ 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;
+
+ if (retval == GROW_NONE && r.contains((int)x, (int)y))
+ retval = MOVE;
+ }
+ return retval;
+ }
+
+ void handleMotion(int edge, float dx, float dy) {
+ Rect r = computeLayout();
+ if (edge == GROW_NONE) {
+ return;
+ } else if (edge == MOVE) {
+ 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;
+
+ 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);
+
+ }
+// Log.v(TAG, "ratio is now " + this.mCropRect.width() / this.mCropRect.height());
+ }
+
+ void moveBy(float dx, float dy) {
+ Rect invalRect = new Rect(mDrawRect);
+
+ mCropRect.offset(dx, dy);
+ 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);
+ }
+
+ private void shift(RectF r, float dx, float dy) {
+ r.left += dx;
+ r.right += dx;
+ r.top += dy;
+ r.bottom += dy;
+ }
+
+ void growBy(float dx, float dy) {
+// Log.v(TAG, "growBy: " + dx + " " + dy + "; rect w/h is " + mCropRect.width() + " / " + mCropRect.height());
+ if (mMaintainAspectRatio) {
+ if (dx != 0) {
+ dy = dx / mInitialAspectRatio;
+ } else if (dy != 0) {
+ dx = dy * mInitialAspectRatio;
+ }
+ }
+
+ 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);
+
+ float widthCap = 25F;
+ if (r.width() < 25) {
+ r.inset(-(25F-r.width())/2F, 0F);
+ }
+ float heightCap = mMaintainAspectRatio ? (widthCap / mInitialAspectRatio) : widthCap;
+ if (r.height() < heightCap) {
+ r.inset(0F, -(heightCap-r.height())/2F);
+ }
+
+ if (r.left < mImageRect.left) {
+ shift(r, mImageRect.left - r.left, 0F);
+ } else if (r.right > mImageRect.right) {
+ shift(r, -(r.right - mImageRect.right), 0);
+ }
+ if (r.top < mImageRect.top) {
+ shift(r, 0F, mImageRect.top - r.top);
+ } else if (r.bottom > mImageRect.bottom) {
+ shift(r, 0F, -(r.bottom - mImageRect.bottom));
+ }
+/*
+ RectF rCandidate = new RectF(r);
+ r.intersect(mImageRect);
+ if (mMaintainAspectRatio) {
+ if (r.left != rCandidate.left) {
+ Log.v(TAG, "bail 1");
+ return;
+ }
+ if (r.right != rCandidate.right) {
+ Log.v(TAG, "bail 2");
+ return;
+ }
+ if (r.top != rCandidate.top) {
+ Log.v(TAG, "bail 3");
+ return;
+ }
+ if (r.bottom != rCandidate.bottom) {
+ Log.v(TAG, "bail 4");
+ return;
+ }
+ }
+*/
+ mCropRect.set(r);
+ mDrawRect = computeLayout();
+ mContext.invalidate();
+ }
+
+ public Rect getCropRect() {
+ return new Rect((int)mCropRect.left, (int)mCropRect.top, (int)mCropRect.right, (int)mCropRect.bottom);
+ }
+
+ 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 (Config.LOGV) Log.v(TAG, "setup... " + imageRect + "; " + cropRect + "; maintain " + maintainAspectRatio + "; circle " + circle);
+ 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;
+ initHighlightView();
+ }
+
+ public void modify(int keyCode, long repeatCount)
+ {
+ float factor = Math.max(.01F, Math.min(.1F, repeatCount * .01F));
+ float widthUnits = factor * (float)mContext.getWidth();
+ float heightUnits = widthUnits;
+
+ switch (keyCode)
+ {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (mMode == ModifyMode.Move)
+ moveBy(-widthUnits, 0);
+ else if (mMode == ModifyMode.Grow)
+ growBy(-widthUnits, 0);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (mMode == ModifyMode.Move)
+ moveBy(widthUnits, 0);
+ else if (mMode == ModifyMode.Grow)
+ growBy(widthUnits, 0);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if (mMode == ModifyMode.Move)
+ moveBy(0, -heightUnits);
+ else if (mMode == ModifyMode.Grow)
+ growBy(0, -heightUnits);
+ break;
+
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if (mMode == ModifyMode.Move)
+ moveBy(0, heightUnits);
+ else if (mMode == ModifyMode.Grow)
+ growBy(0, heightUnits);
+ break;
+ }
+ }
+
+ enum ModifyMode { None, Move,Grow };
+
+ ModifyMode mMode = ModifyMode.None;
+
+ Rect mDrawRect;
+ RectF mImageRect;
+ RectF mCropRect;
+ Matrix mMatrix;
+
+ boolean mMaintainAspectRatio = false;
+ float mInitialAspectRatio;
+ boolean mCircle = false;
+
+ Drawable mResizeDrawableWidth, mResizeDrawableHeight, mResizeDrawableDiagonal;
+
+ Paint mFocusPaint = new Paint();
+ Paint mNoFocusPaint = new Paint();
+ Paint mOutlinePaint = new Paint();
+}
diff --git a/src/com/android/camera/ImageGallery2.java b/src/com/android/camera/ImageGallery2.java
new file mode 100644
index 0000000..c8abdae
--- /dev/null
+++ b/src/com/android/camera/ImageGallery2.java
@@ -0,0 +1,1734 @@
+/*
+ * 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 android.content.BroadcastReceiver;
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.GestureDetector;
+import android.view.GestureDetector.SimpleOnGestureListener;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.Window;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.preference.PreferenceManager;
+import android.widget.Scroller;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class ImageGallery2 extends Activity {
+ private static final String TAG = "ImageGallery2";
+ private ImageManager.IImageList mAllImages;
+ private int mInclusion;
+ private boolean mSortAscending = false;
+ private View mNoImagesView;
+ public final static int CROP_MSG = 2;
+ public final static int VIEW_MSG = 3;
+
+ private static final String INSTANCE_STATE_TAG = "scrollY";
+
+ private Dialog mMediaScanningDialog;
+
+ private MenuItem mFlipItem;
+ private SharedPreferences mPrefs;
+
+ public ImageGallery2() {
+ }
+
+ BroadcastReceiver mReceiver = null;
+
+ Handler mHandler = new Handler();
+ boolean mLayoutComplete;
+ boolean mPausing = false;
+ boolean mStopThumbnailChecking = false;
+
+ CameraThread mThumbnailCheckThread;
+ GridViewSpecial mGvs;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ if (Config.LOGV) Log.v(TAG, "onCreate");
+ super.onCreate(icicle);
+
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ requestWindowFeature(Window.FEATURE_CUSTOM_TITLE); // must be called before setContentView()
+ setContentView(R.layout.image_gallery_2);
+
+ getWindow().setFeatureInt(Window.FEATURE_CUSTOM_TITLE, R.layout.custom_gallery_title);
+ if (Config.LOGV)
+ Log.v(TAG, "findView... " + findViewById(R.id.loading_indicator));
+
+ mGvs = (GridViewSpecial) findViewById(R.id.grid);
+ mGvs.requestFocus();
+
+ if (!isPickIntent()) {
+ mGvs.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ if (mSelectedImageGetter.getCurrentImage() == null)
+ return;
+
+ menu.add(0, 0, 0, R.string.view).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ mGvs.onSelect(mGvs.mCurrentSelection);
+ return true;
+ }
+ });
+
+ menu.setHeaderTitle(R.string.context_menu_header);
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ MenuHelper.MenuItemsResult r = MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ ImageGallery2.this,
+ mHandler,
+ mDeletePhotoRunnable,
+ new MenuHelper.MenuInvoker() {
+ public void run(MenuHelper.MenuCallback cb) {
+ cb.run(mSelectedImageGetter.getCurrentImageUri(), mSelectedImageGetter.getCurrentImage());
+
+ mGvs.clearCache();
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ });
+ if (r != null)
+ r.gettingReadyToOpen(menu, mSelectedImageGetter.getCurrentImage());
+
+ addSlideShowMenu(menu, 1000);
+ }
+
+ if ((mInclusion & ImageManager.INCLUDE_VIDEOS) != 0) {
+ MenuHelper.MenuItemsResult r = MenuHelper.addVideoMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ ImageGallery2.this,
+ mHandler,
+ mSelectedImageGetter,
+ new Runnable() {
+ public void run() {
+ ImageManager.IImage image = mSelectedImageGetter.getCurrentImage();
+ if (image != null) {
+ mGvs.clearCache();
+ mAllImages.removeImage(mSelectedImageGetter.getCurrentImage());
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ }
+ },
+ null, null);
+ if (r != null)
+ r.gettingReadyToOpen(menu, mSelectedImageGetter.getCurrentImage());
+ }
+ }
+ });
+ }
+ }
+
+ private MenuItem addSlideShowMenu(Menu menu, int position) {
+ return menu.add(0, 207, position, R.string.slide_show)
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ ImageManager.IImage img = mSelectedImageGetter.getCurrentImage();
+ if (img == null) {
+ img = mAllImages.getImageAt(0);
+ if (img == null) {
+ return true;
+ }
+ }
+ Uri targetUri = img.fullSizeImageUri();
+ Uri thisUri = getIntent().getData();
+ if (thisUri != null) {
+ String bucket = thisUri.getQueryParameter("bucketId");
+ if (bucket != null) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+ intent.putExtra("slideshow", true);
+ startActivity(intent);
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_slideshow);
+ }
+
+ private Runnable mDeletePhotoRunnable = new Runnable() {
+ public void run() {
+ mGvs.clearCache();
+ mAllImages.removeImage(mSelectedImageGetter.getCurrentImage());
+ mGvs.invalidate();
+ mGvs.start();
+ mNoImagesView.setVisibility(mAllImages.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+ };
+
+ private SelectedImageGetter mSelectedImageGetter = new SelectedImageGetter() {
+ public Uri getCurrentImageUri() {
+ ImageManager.IImage image = getCurrentImage();
+ if (image != null)
+ return image.fullSizeImageUri();
+ else
+ return null;
+ }
+ public ImageManager.IImage getCurrentImage() {
+ int currentSelection = mGvs.mCurrentSelection;
+ if (currentSelection < 0 || currentSelection >= mAllImages.getCount())
+ return null;
+ else
+ return mAllImages.getImageAt(currentSelection);
+ }
+ };
+
+ @Override
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mTargetScroll = mGvs.getScrollY();
+ }
+
+ private Runnable mLongPressCallback = new Runnable() {
+ public void run() {
+ mGvs.showContextMenu();
+ }
+ };
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ // The keyUp doesn't get called when the longpress menu comes up. We only get here when the user
+ // lets go of the center key before the longpress menu comes up.
+ mHandler.removeCallbacks(mLongPressCallback);
+
+ // open the photo
+ if (mSelectedImageGetter.getCurrentImage() != null) {
+ mGvs.onSelect(mGvs.mCurrentSelection);
+ }
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ boolean handled = true;
+ int sel = mGvs.mCurrentSelection;
+ int columns = mGvs.mCurrentSpec.mColumns;
+ int count = mAllImages.getCount();
+ if (mGvs.mShowSelection) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ if (sel != count && (sel % columns < columns - 1)) {
+ sel += 1;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ if (sel > 0 && (sel % columns != 0)) {
+ sel -= 1;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_UP:
+ if ((sel / columns) != 0) {
+ sel -= columns;
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ if ((sel / columns) != (sel+columns / columns)) {
+ sel = Math.min(count-1, sel + columns);
+ }
+ break;
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ mHandler.postDelayed(mLongPressCallback, ViewConfiguration.getLongPressTimeout());
+ break;
+ case KeyEvent.KEYCODE_DEL:
+ MenuHelper.deletePhoto(this, mDeletePhotoRunnable);
+ break;
+ default:
+ handled = false;
+ break;
+ }
+ } else {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ case KeyEvent.KEYCODE_DPAD_UP:
+ case KeyEvent.KEYCODE_DPAD_DOWN:
+ int [] range = new int[2];
+ GridViewSpecial.ImageBlockManager ibm = mGvs.mImageBlockManager;
+ if (ibm != null) {
+ mGvs.mImageBlockManager.getVisibleRange(range);
+ int topPos = range[0];
+ android.graphics.Rect r = mGvs.getRectForPosition(topPos);
+ if (r.top < mGvs.getScrollY())
+ topPos += columns;
+ topPos = Math.min(count - 1, topPos);
+ sel = topPos;
+ }
+ break;
+ default:
+ handled = false;
+ break;
+ }
+ }
+ if (handled) {
+ mGvs.select(sel);
+ return true;
+ }
+ else
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+ }
+
+ private void launchCropperOrFinish(ImageManager.IImage img) {
+ Bundle myExtras = getIntent().getExtras();
+
+ String cropValue = myExtras != null ? myExtras.getString("crop") : null;
+ if (cropValue != null) {
+ Bundle newExtras = new Bundle();
+ if (cropValue.equals("circle"))
+ newExtras.putString("circleCrop", "true");
+
+ Intent cropIntent = new Intent();
+ cropIntent.setData(img.fullSizeImageUri());
+ cropIntent.setClass(this, CropImage.class);
+ cropIntent.putExtras(newExtras);
+
+ /* pass through any extras that were passed in */
+ cropIntent.putExtras(myExtras);
+ if (Config.LOGV) Log.v(TAG, "startSubActivity " + cropIntent);
+ startActivityForResult(cropIntent, CROP_MSG);
+ } else {
+ Intent result = new Intent(null, img.fullSizeImageUri());
+ if (myExtras != null && myExtras.getString("return-data") != null) {
+ Bitmap bitmap = img.fullSizeBitmap(1000);
+ if (bitmap != null)
+ result.putExtra("data", bitmap);
+ }
+ setResult(RESULT_OK, result);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (Config.LOGV)
+ Log.v(TAG, "onActivityResult: " + requestCode + "; resultCode is " + resultCode + "; data is " + data);
+ switch (requestCode) {
+ case CROP_MSG: {
+ if (Config.LOGV) Log.v(TAG, "onActivityResult " + data);
+ setResult(resultCode, data);
+ finish();
+ break;
+ }
+ case VIEW_MSG: {
+ if (Config.LOGV)
+ Log.v(TAG, "got VIEW_MSG with " + data);
+ ImageManager.IImage img = mAllImages.getImageForUri(data.getData());
+ launchCropperOrFinish(img);
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mPausing = true;
+ stopCheckingThumbnails();
+ mAllImages.deactivate();
+ mGvs.onPause();
+
+ if (mReceiver != null) {
+ unregisterReceiver(mReceiver);
+ mReceiver = null;
+ }
+ }
+
+ private void rebake(boolean unmounted, boolean scanning) {
+ stopCheckingThumbnails();
+ mGvs.clearCache();
+ if (mAllImages != null) {
+ mAllImages.deactivate();
+ mAllImages = null;
+ }
+ if (mMediaScanningDialog != null) {
+ mMediaScanningDialog.cancel();
+ mMediaScanningDialog = null;
+ }
+ if (scanning) {
+ mMediaScanningDialog = ProgressDialog.show(
+ this,
+ null,
+ getResources().getString(R.string.wait),
+ true,
+ true);
+ mAllImages = ImageManager.instance().emptyImageList();
+ } else {
+ mAllImages = allImages(!unmounted);
+ if (Config.LOGV)
+ Log.v(TAG, "mAllImages is now " + mAllImages);
+ mGvs.init(mHandler);
+ mGvs.start();
+ mGvs.requestLayout();
+ checkThumbnails();
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle state) {
+ super.onSaveInstanceState(state);
+ mTargetScroll = mGvs.getScrollY();
+ state.putInt(INSTANCE_STATE_TAG, mTargetScroll);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle state) {
+ super.onRestoreInstanceState(state);
+ mTargetScroll = state.getInt(INSTANCE_STATE_TAG, 0);
+ }
+
+ int mTargetScroll;
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ try {
+ mGvs.setSizeChoice(Integer.parseInt(mPrefs.getString("pref_gallery_size_key", "1")), mTargetScroll);
+
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ if (sortOrder != null) {
+ mSortAscending = sortOrder.equals("ascending");
+ }
+ } catch (Exception ex) {
+
+ }
+ mPausing = false;
+
+ // install an intent filter to receive SD card related events.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_MOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);
+ intentFilter.addAction(Intent.ACTION_MEDIA_EJECT);
+ intentFilter.addDataScheme("file");
+
+ mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Config.LOGV) Log.v(TAG, "onReceiveIntent " + intent.getAction());
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ // SD card available
+ // TODO put up a "please wait" message
+ // TODO also listen for the media scanner finished message
+ } else if (action.equals(Intent.ACTION_MEDIA_UNMOUNTED)) {
+ // SD card unavailable
+ if (Config.LOGV) Log.v(TAG, "sd card no longer available");
+ Toast.makeText(ImageGallery2.this, getResources().getString(R.string.wait), 5000);
+ rebake(true, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_STARTED)) {
+ Toast.makeText(ImageGallery2.this, getResources().getString(R.string.wait), 5000);
+ rebake(false, true);
+ } else if (action.equals(Intent.ACTION_MEDIA_SCANNER_FINISHED)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_SCANNER_FINISHED");
+ rebake(false, false);
+ } else if (action.equals(Intent.ACTION_MEDIA_EJECT)) {
+ if (Config.LOGV)
+ Log.v(TAG, "rebake because of ACTION_MEDIA_EJECT");
+ rebake(true, false);
+ }
+ }
+ };
+ registerReceiver(mReceiver, intentFilter);
+
+ MenuHelper.requestOrientation(this, mPrefs);
+
+ rebake(false, ImageManager.isMediaScannerScanning(this));
+ }
+
+ private void stopCheckingThumbnails() {
+ mStopThumbnailChecking = true;
+ if (mThumbnailCheckThread != null) {
+ mThumbnailCheckThread.join();
+ }
+ mStopThumbnailChecking = false;
+ }
+
+ private void checkThumbnails() {
+ final long startTime = System.currentTimeMillis();
+ final long t1 = System.currentTimeMillis();
+ mThumbnailCheckThread = new CameraThread(new Runnable() {
+ public void run() {
+ android.content.res.Resources resources = getResources();
+ boolean loadingVideos = mInclusion == ImageManager.INCLUDE_VIDEOS;
+ final TextView progressTextView = (TextView) findViewById(R.id.loading_text);
+ final String progressTextFormatString = resources.getString(R.string.loading_progress_format_string);
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock mWakeLock =
+ pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+ "ImageGallery2.checkThumbnails");
+ mWakeLock.acquire();
+ ImageManager.IImageList.ThumbCheckCallback r = new ImageManager.IImageList.ThumbCheckCallback() {
+ boolean mDidSetProgress = false;
+
+ public boolean checking(final int count, final int maxCount) {
+ if (mStopThumbnailChecking) {
+ return false;
+ }
+
+ if (!mLayoutComplete) {
+ return true;
+ }
+
+ if (!mDidSetProgress) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ findViewById(R.id.loading_indicator).setVisibility(View.VISIBLE);
+ }
+ });
+ mDidSetProgress = true;
+ }
+ mGvs.postInvalidate();
+
+ if (System.currentTimeMillis() - startTime > 1000) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ String s = String.format(progressTextFormatString, maxCount - count);
+ progressTextView.setText(s);
+ }
+ });
+ }
+
+ return !mPausing;
+ }
+ };
+ allImages(true).checkThumbnails(r);
+ mWakeLock.release();
+ mThumbnailCheckThread = null;
+ mHandler.post(new Runnable() {
+ public void run() {
+ findViewById(R.id.loading_indicator).setVisibility(View.GONE);
+ }
+ });
+ long t2 = System.currentTimeMillis();
+ if (Config.LOGV)
+ Log.v(TAG, "check thumbnails thread finishing; took " + (t2-t1));
+ }
+ });
+
+ mThumbnailCheckThread.setName("check_thumbnails");
+ mThumbnailCheckThread.start();
+ mThumbnailCheckThread.toBackground();
+
+ ImageManager.IImageList list = allImages(true);
+ mNoImagesView.setVisibility(list.getCount() > 0 ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(android.view.Menu menu) {
+ MenuItem item;
+ if (false) {
+ if ((mInclusion & ImageManager.INCLUDE_IMAGES) != 0) {
+ item = menu.add(0, 0, 0, R.string.upload_all);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ UploadAction.uploadImage(ImageGallery2.this, null);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_upload);
+ }
+ }
+ addSlideShowMenu(menu, 0);
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, this, mPrefs);
+
+ item = menu.add(0, 0, 1000, R.string.camerasettings);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(ImageGallery2.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ });
+ item.setAlphabeticShortcut('p');
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(android.view.Menu menu) {
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ return true;
+ }
+
+ private synchronized ImageManager.IImageList allImages(boolean assumeMounted) {
+ if (mAllImages == null) {
+ mNoImagesView = findViewById(R.id.no_images);
+
+ mInclusion = ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS;
+ ImageManager.DataLocation location = ImageManager.DataLocation.ALL;
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ String type = intent.resolveType(this);
+ if (Config.LOGV)
+ Log.v(TAG, "allImages... type is " + type);
+ TextView leftText = (TextView) findViewById(R.id.left_text);
+ if (type != null) {
+ if (type.equals("vnd.android.cursor.dir/image") || type.equals("image/*")) {
+ mInclusion = ImageManager.INCLUDE_IMAGES;
+ if (isPickIntent())
+ leftText.setText(R.string.pick_photos_gallery_title);
+ else
+ leftText.setText(R.string.photos_gallery_title);
+ }
+ if (type.equals("vnd.android.cursor.dir/video") || type.equals("video/*")) {
+ }
+ }
+ Bundle extras = intent.getExtras();
+ String title = extras!= null ? extras.getString("windowTitle") : null;
+ if (title != null && title.length() > 0) {
+ leftText.setText(title);
+ }
+
+ if (extras != null && extras.getBoolean("pick-drm")) {
+ Log.d(TAG, "pick-drm is true");
+ mInclusion = ImageManager.INCLUDE_DRM_IMAGES;
+ location = ImageManager.DataLocation.INTERNAL;
+ }
+ }
+ if (Config.LOGV)
+ Log.v(TAG, "computing images... mSortAscending is " + mSortAscending + "; assumeMounted is " + assumeMounted);
+ Uri uri = getIntent().getData();
+ if (!assumeMounted) {
+ mAllImages = ImageManager.instance().emptyImageList();
+ } else {
+ mAllImages = ImageManager.instance().allImages(
+ ImageGallery2.this,
+ getContentResolver(),
+ ImageManager.DataLocation.NONE,
+ mInclusion,
+ mSortAscending ? ImageManager.SORT_ASCENDING : ImageManager.SORT_DESCENDING,
+ uri != null ? uri.getQueryParameter("bucketId") : null);
+ }
+ }
+ return mAllImages;
+ }
+
+ public static class GridViewSpecial extends View {
+ private ImageGallery2 mGallery;
+ private Paint mGridViewPaint = new Paint();
+
+ private ImageBlockManager mImageBlockManager;
+ private Handler mHandler;
+
+ private LayoutSpec mCurrentSpec;
+ private boolean mShowSelection = false;
+ private int mCurrentSelection = -1;
+
+ private boolean mDirectionBiasDown = true;
+ private final static boolean sDump = false;
+
+ class LayoutSpec {
+ LayoutSpec(int cols, int w, int h, int leftEdgePadding, int rightEdgePadding, int intercellSpacing) {
+ mColumns = cols;
+ mCellWidth = w;
+ mCellHeight = h;
+ mLeftEdgePadding = leftEdgePadding;
+ mRightEdgePadding = rightEdgePadding;
+ mCellSpacing = intercellSpacing;
+ }
+ int mColumns;
+ int mCellWidth, mCellHeight;
+ int mLeftEdgePadding, mRightEdgePadding;
+ int mCellSpacing;
+ };
+
+ private LayoutSpec [] mCellSizeChoices = new LayoutSpec[] {
+ new LayoutSpec(0, 67, 67, 14, 14, 8),
+ new LayoutSpec(0, 92, 92, 14, 14, 8),
+ };
+ private int mSizeChoice = 1;
+
+ // Use a number like 100 or 200 here to allow the user to
+ // overshoot the start (top) or end (bottom) of the gallery.
+ // After overshooting the gallery will animate back to the
+ // appropriate location.
+ private int mMaxOvershoot = 0; // 100;
+ private int mMaxScrollY;
+ private int mMinScrollY;
+
+ private boolean mFling = true;
+ private Scroller mScroller = null;
+
+ private GestureDetector mGestureDetector;
+
+ public void dump() {
+ if (Config.LOGV){
+ Log.v(TAG, "mSizeChoice is " + mCellSizeChoices[mSizeChoice]);
+ Log.v(TAG, "mCurrentSpec.width / mCellHeight are " + mCurrentSpec.mCellWidth + " / " + mCurrentSpec.mCellHeight);
+ }
+ mImageBlockManager.dump();
+ }
+
+ private void init(Context context) {
+ mGridViewPaint.setColor(0xFF000000);
+ mGallery = (ImageGallery2) context;
+
+ setVerticalScrollBarEnabled(true);
+ initializeScrollbars(context.obtainStyledAttributes(android.R.styleable.View));
+
+ mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(MotionEvent e) {
+ if (mScroller != null && !mScroller.isFinished()) {
+ mScroller.forceFinished(true);
+ return false;
+ }
+
+ int pos = computeSelectedIndex(e);
+ if (pos >= 0 && pos < mGallery.mAllImages.getCount()) {
+ select(pos);
+ } else {
+ select(-1);
+ }
+ if (mImageBlockManager != null)
+ mImageBlockManager.repaintSelection(mCurrentSelection);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ final float maxVelocity = 2500;
+ if (velocityY > maxVelocity)
+ velocityY = maxVelocity;
+ else if (velocityY < -maxVelocity)
+ velocityY = -maxVelocity;
+
+ select(-1);
+ if (mFling) {
+ mScroller = new Scroller(getContext());
+ mScroller.fling(0, mScrollY, 0, -(int)velocityY, 0, 0, 0, mMaxScrollY);
+ computeScroll();
+ }
+ return true;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ performLongClick();
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ select(-1);
+ scrollBy(0, (int)distanceY);
+ invalidate();
+ return true;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ super.onShowPress(e);
+ }
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ int index = computeSelectedIndex(e);
+ if (index >= 0 && index < mGallery.mAllImages.getCount()) {
+ onSelect(index);
+ return true;
+ }
+ return false;
+ }
+ });
+// mGestureDetector.setIsLongpressEnabled(false);
+ }
+
+ public GridViewSpecial(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(context);
+ }
+
+ public GridViewSpecial(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public GridViewSpecial(Context context) {
+ super(context);
+ init(context);
+ }
+
+ @Override
+ protected int computeVerticalScrollRange() {
+ return mMaxScrollY + getHeight();
+ }
+
+ public void setSizeChoice(int choice, int scrollY) {
+ mSizeChoice = choice;
+ clearCache();
+ scrollTo(0, scrollY);
+ requestLayout();
+ invalidate();
+ }
+
+ public void select(int newSel) {
+ int oldSel = mCurrentSelection;
+ if (oldSel == newSel)
+ return;
+
+ mShowSelection = (newSel != -1);
+ mCurrentSelection = newSel;
+ if (mImageBlockManager != null) {
+ mImageBlockManager.repaintSelection(oldSel);
+ mImageBlockManager.repaintSelection(newSel);
+ }
+
+ if (newSel != -1)
+ ensureVisible(newSel);
+ }
+
+ private void ensureVisible(int pos) {
+ android.graphics.Rect r = getRectForPosition(pos);
+ int top = getScrollY();
+ int bot = top + getHeight();
+
+ if (r.bottom > bot) {
+ mScroller = new Scroller(getContext());
+ mScroller.startScroll(mScrollX, mScrollY, 0, r.bottom - getHeight() - mScrollY, 200);
+ computeScroll();
+ } else if (r.top < top) {
+ mScroller = new Scroller(getContext());
+ mScroller.startScroll(mScrollX, mScrollY, 0, r.top - mScrollY, 200);
+ computeScroll();
+ }
+ invalidate();
+ }
+
+ public void start() {
+ if (mGallery.mLayoutComplete) {
+ if (mImageBlockManager == null) {
+ mImageBlockManager = new ImageBlockManager();
+ mImageBlockManager.moveDataWindow(true, true);
+ }
+ }
+ }
+
+ public void onPause() {
+ mScroller = null;
+ if (mImageBlockManager != null) {
+ mImageBlockManager.onPause();
+ mImageBlockManager = null;
+ }
+ }
+
+ public void clearCache() {
+ if (mImageBlockManager != null) {
+ mImageBlockManager.onPause();
+ mImageBlockManager = null;
+ }
+ }
+
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mGallery.isFinishing() || mGallery.mPausing) {
+ return;
+ }
+
+ clearCache();
+
+ mCurrentSpec = mCellSizeChoices[mSizeChoice];
+ int oldColumnCount = mCurrentSpec.mColumns;
+
+ int width = right - left;
+ mCurrentSpec.mColumns = 1;
+ width -= mCurrentSpec.mCellWidth;
+ mCurrentSpec.mColumns += width / (mCurrentSpec.mCellWidth + mCurrentSpec.mCellSpacing);
+
+ mCurrentSpec.mLeftEdgePadding = ((right - left) - ((mCurrentSpec.mColumns - 1) * mCurrentSpec.mCellSpacing) - (mCurrentSpec.mColumns * mCurrentSpec.mCellWidth)) / 2;
+ mCurrentSpec.mRightEdgePadding = mCurrentSpec.mLeftEdgePadding;
+
+ int rows = (mGallery.mAllImages.getCount() + mCurrentSpec.mColumns - 1) / mCurrentSpec.mColumns;
+ mMaxScrollY = mCurrentSpec.mCellSpacing + (rows * (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight)) - (bottom - top) + mMaxOvershoot;
+ mMinScrollY = 0 - mMaxOvershoot;
+
+ mGallery.mLayoutComplete = true;
+
+ start();
+
+ if (mGallery.mSortAscending && mGallery.mTargetScroll == 0) {
+ scrollTo(0, mMaxScrollY - mMaxOvershoot);
+ } else {
+ if (oldColumnCount != 0) {
+ int y = mGallery.mTargetScroll * oldColumnCount / mCurrentSpec.mColumns;
+ Log.v(TAG, "target was " + mGallery.mTargetScroll + " now " + y);
+ scrollTo(0, y);
+ }
+ }
+ }
+
+ Bitmap scaleTo(int width, int height, Bitmap b) {
+ Matrix m = new Matrix();
+ m.setScale((float)width/64F, (float)height/64F);
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ if (b2 != b)
+ b.recycle();
+ return b2;
+ }
+
+ private class ImageBlockManager {
+ private ImageLoader mLoader;
+ private int mBlockCacheFirstBlockNumber = 0;
+
+ // mBlockCache is an array with a starting point which is not necessaryily
+ // zero. The first element of the array is indicated by mBlockCacheStartOffset.
+ private int mBlockCacheStartOffset = 0;
+ private ImageBlock [] mBlockCache;
+
+ private static final int sRowsPerPage = 6; // should compute this
+
+ private static final int sPagesPreCache = 2;
+ private static final int sPagesPostCache = 2;
+
+ private int mWorkCounter = 0;
+ private boolean mDone = false;
+
+ private Thread mWorkerThread;
+ private Bitmap mErrorBitmap;
+
+ public void dump() {
+ synchronized (ImageBlockManager.this) {
+ StringBuilder line1 = new StringBuilder();
+ StringBuilder line2 = new StringBuilder();
+ if (Config.LOGV)
+ Log.v(TAG, ">>> mBlockCacheFirstBlockNumber: " + mBlockCacheFirstBlockNumber + " " + mBlockCacheStartOffset);
+ for (int i = 0; i < mBlockCache.length; i++) {
+ int index = (mBlockCacheStartOffset + i) % mBlockCache.length;
+ ImageBlock block = mBlockCache[index];
+ block.dump(line1, line2);
+ }
+ if (Config.LOGV){
+ Log.v(TAG, line1.toString());
+ Log.v(TAG, line2.toString());
+ }
+ }
+ }
+
+ ImageBlockManager() {
+ mLoader = new ImageLoader(mHandler, 1);
+
+ mBlockCache = new ImageBlock[sRowsPerPage * (sPagesPreCache + sPagesPostCache + 1)];
+ for (int i = 0; i < mBlockCache.length; i++) {
+ mBlockCache[i] = new ImageBlock();
+ }
+
+ mWorkerThread = new Thread(new Runnable() {
+ public void run() {
+ while (true) {
+ int workCounter;
+ synchronized (ImageBlockManager.this) {
+ workCounter = mWorkCounter;
+ }
+ if (mDone) {
+ if (Config.LOGV)
+ Log.v(TAG, "stopping the loader here " + Thread.currentThread().getName());
+ if (mLoader != null) {
+ mLoader.stop();
+ }
+ if (mBlockCache != null) {
+ for (int i = 0; i < mBlockCache.length; i++) {
+ ImageBlock block = mBlockCache[i];
+ if (block != null) {
+ block.recycleBitmaps();
+ mBlockCache[i] = null;
+ }
+ }
+ }
+ mBlockCache = null;
+ mBlockCacheStartOffset = 0;
+ mBlockCacheFirstBlockNumber = 0;
+
+ break;
+ }
+
+ loadNext();
+
+ synchronized (ImageBlockManager.this) {
+ if (workCounter == mWorkCounter) {
+ try {
+ ImageBlockManager.this.wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ }
+ }
+ });
+ mWorkerThread.setName("image-block-manager");
+ mWorkerThread.start();
+ }
+
+ // Create this bitmap lazily, and only once for all the ImageBlocks to use
+ public Bitmap getErrorBitmap() {
+ if (mErrorBitmap == null) {
+ mErrorBitmap = BitmapFactory.decodeResource(GridViewSpecial.this.getResources(),
+ android.R.drawable.ic_menu_report_image);
+ }
+ return mErrorBitmap;
+ }
+
+ private ImageBlock getBlockForPos(int pos) {
+ synchronized (ImageBlockManager.this) {
+ int blockNumber = pos / mCurrentSpec.mColumns;
+ int delta = blockNumber - mBlockCacheFirstBlockNumber;
+ if (delta >= 0 && delta < mBlockCache.length) {
+ int index = (mBlockCacheStartOffset + delta) % mBlockCache.length;
+ ImageBlock b = mBlockCache[index];
+ return b;
+ }
+ }
+ return null;
+ }
+
+ private void repaintSelection(int pos) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlock b = getBlockForPos(pos);
+ if (b != null) {
+ b.repaintSelection();
+ }
+ }
+ }
+
+ private void onPause() {
+ synchronized (ImageBlockManager.this) {
+ mDone = true;
+ ImageBlockManager.this.notify();
+ }
+ if (mWorkerThread != null) {
+ try {
+ mWorkerThread.join();
+ mWorkerThread = null;
+ } catch (InterruptedException ex) {
+ //
+ }
+ }
+ Log.v(TAG, "/ImageBlockManager.onPause");
+ }
+
+ private void getVisibleRange(int [] range) {
+ // try to work around a possible bug in the VM wherein this appears to be null
+ try {
+ synchronized (ImageBlockManager.this) {
+ int blockLength = mBlockCache.length;
+ boolean lookingForStart = true;
+ ImageBlock prevBlock = null;
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ ImageBlock block = mBlockCache[index];
+ if (lookingForStart) {
+ if (block.mIsVisible) {
+ range[0] = block.mBlockNumber * mCurrentSpec.mColumns;
+ lookingForStart = false;
+ }
+ } else {
+ if (!block.mIsVisible || i == blockLength - 1) {
+ range[1] = (prevBlock.mBlockNumber * mCurrentSpec.mColumns) + mCurrentSpec.mColumns - 1;
+ break;
+ }
+ }
+ prevBlock = block;
+ }
+ }
+ } catch (NullPointerException ex) {
+ Log.e(TAG, "this is somewhat null, what up?");
+ range[0] = range[1] = 0;
+ }
+ }
+
+ private void loadNext() {
+ final int blockHeight = (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight);
+
+ final int firstVisBlock = Math.max(0, (mScrollY - mCurrentSpec.mCellSpacing) / blockHeight);
+ final int lastVisBlock = (mScrollY - mCurrentSpec.mCellSpacing + getHeight()) / blockHeight;
+
+// Log.v(TAG, "firstVisBlock == " + firstVisBlock + "; lastVisBlock == " + lastVisBlock);
+
+ synchronized (ImageBlockManager.this) {
+ ImageBlock [] blocks = mBlockCache;
+ int numBlocks = blocks.length;
+ if (mDirectionBiasDown) {
+ int first = (mBlockCacheStartOffset + (firstVisBlock - mBlockCacheFirstBlockNumber)) % blocks.length;
+ for (int i = 0; i < numBlocks; i++) {
+ int j = first + i;
+ if (j >= numBlocks)
+ j -= numBlocks;
+ ImageBlock b = blocks[j];
+ if (b.startLoading() > 0)
+ break;
+ }
+ } else {
+ int first = (mBlockCacheStartOffset + (lastVisBlock - mBlockCacheFirstBlockNumber)) % blocks.length;
+ for (int i = 0; i < numBlocks; i++) {
+ int j = first - i;
+ if (j < 0)
+ j += numBlocks;
+ ImageBlock b = blocks[j];
+ if (b.startLoading() > 0)
+ break;
+ }
+ }
+ if (sDump)
+ this.dump();
+ }
+ }
+
+ private void moveDataWindow(boolean directionBiasDown, boolean forceRefresh) {
+ final int blockHeight = (mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight);
+
+ final int firstVisBlock = (mScrollY - mCurrentSpec.mCellSpacing) / blockHeight;
+ final int lastVisBlock = (mScrollY - mCurrentSpec.mCellSpacing + getHeight()) / blockHeight;
+
+ final int preCache = sPagesPreCache;
+ final int startBlock = Math.max(0, firstVisBlock - (preCache * sRowsPerPage));
+
+// Log.v(TAG, "moveDataWindow directionBiasDown == " + directionBiasDown + "; preCache is " + preCache);
+ synchronized (ImageBlockManager.this) {
+ boolean any = false;
+ ImageBlock [] blocks = mBlockCache;
+ int numBlocks = blocks.length;
+
+ int delta = startBlock - mBlockCacheFirstBlockNumber;
+
+ mBlockCacheFirstBlockNumber = startBlock;
+ if (Math.abs(delta) > numBlocks || forceRefresh) {
+ for (int i = 0; i < numBlocks; i++) {
+ int blockNum = startBlock + i;
+ blocks[i].setStart(blockNum);
+ any = true;
+ }
+ mBlockCacheStartOffset = 0;
+ } else if (delta > 0) {
+ mBlockCacheStartOffset += delta;
+ if (mBlockCacheStartOffset >= numBlocks)
+ mBlockCacheStartOffset -= numBlocks;
+
+ for (int i = delta; i > 0; i--) {
+ int index = (mBlockCacheStartOffset + numBlocks - i) % numBlocks;
+ int blockNum = mBlockCacheFirstBlockNumber + numBlocks - i;
+ blocks[index].setStart(blockNum);
+ any = true;
+ }
+ } else if (delta < 0) {
+ mBlockCacheStartOffset += delta;
+ if (mBlockCacheStartOffset < 0)
+ mBlockCacheStartOffset += numBlocks;
+
+ for (int i = 0; i < -delta; i++) {
+ int index = (mBlockCacheStartOffset + i) % numBlocks;
+ int blockNum = mBlockCacheFirstBlockNumber + i;
+ blocks[index].setStart(blockNum);
+ any = true;
+ }
+ }
+
+ for (int i = 0; i < numBlocks; i++) {
+ int index = (mBlockCacheStartOffset + i) % numBlocks;
+ ImageBlock block = blocks[index];
+ int blockNum = block.mBlockNumber; // mBlockCacheFirstBlockNumber + i;
+ boolean isVis = blockNum >= firstVisBlock && blockNum <= lastVisBlock;
+// Log.v(TAG, "blockNum " + blockNum + " setting vis to " + isVis);
+ block.setVisibility(isVis);
+ }
+
+ if (sDump)
+ mImageBlockManager.dump();
+
+ if (any) {
+ ImageBlockManager.this.notify();
+ mWorkCounter += 1;
+ }
+ }
+ if (sDump)
+ dump();
+ }
+
+ private void check() {
+ ImageBlock [] blocks = mBlockCache;
+ int blockLength = blocks.length;
+
+ // check the results
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ if (blocks[index].mBlockNumber != mBlockCacheFirstBlockNumber + i) {
+ if (blocks[index].mBlockNumber != -1)
+ Log.e(TAG, "at " + i + " block cache corrupted; found " + blocks[index].mBlockNumber + " but wanted " + (mBlockCacheFirstBlockNumber + i) + "; offset is " + mBlockCacheStartOffset);
+ }
+ }
+ if (true) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < blockLength; i++) {
+ int index = (mBlockCacheStartOffset + i) % blockLength;
+ ImageBlock b = blocks[index];
+ if (b.mRequestedMask != 0)
+ sb.append("X");
+ else
+ sb.append(String.valueOf(b.mBlockNumber) + " ");
+ }
+ if (Config.LOGV)
+ Log.v(TAG, "moveDataWindow " + sb.toString());
+ }
+ }
+
+ void doDraw(Canvas canvas) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlockManager.ImageBlock [] blocks = mBlockCache;
+ int blockCount = 0;
+
+ if (blocks[0] == null) {
+ return;
+ }
+
+ final int thisHeight = getHeight();
+ final int thisWidth = getWidth();
+ final int height = blocks[0].mBitmap.getHeight();
+ final int scrollPos = mScrollY;
+
+ int currentBlock = (scrollPos < 0) ? ((scrollPos-height+1) / height) : (scrollPos / height);
+
+ while (true) {
+ final int yPos = currentBlock * height;
+ if (yPos >= scrollPos + thisHeight)
+ break;
+
+ if (currentBlock < 0) {
+ canvas.drawRect(0, yPos, thisWidth, 0, mGridViewPaint);
+ currentBlock += 1;
+ continue;
+ }
+ int effectiveOffset = (mBlockCacheStartOffset + (currentBlock++ - mBlockCacheFirstBlockNumber)) % blocks.length;
+ if (effectiveOffset < 0 || effectiveOffset >= blocks.length) {
+ break;
+ }
+
+ ImageBlock block = blocks[effectiveOffset];
+ if (block == null) {
+ break;
+ }
+ synchronized (block) {
+ Bitmap b = block.mBitmap;
+ if (b == null) {
+ break;
+ }
+ canvas.drawBitmap(b, 0, yPos, mGridViewPaint);
+ blockCount += 1;
+ }
+ }
+ }
+ }
+
+ int blockHeight() {
+ return mCurrentSpec.mCellSpacing + mCurrentSpec.mCellHeight;
+ }
+
+ private class ImageBlock {
+ Drawable mCellOutline;
+ Bitmap mBitmap = Bitmap.createBitmap(getWidth(), blockHeight(),
+ Bitmap.Config.RGB_565);;
+ Canvas mCanvas = new Canvas(mBitmap);
+ Paint mPaint = new Paint();
+
+ int mBlockNumber;
+ int mRequestedMask; // columns which have been requested to the loader
+ int mCompletedMask; // columns which have been completed from the loader
+ boolean mIsVisible;
+
+ public void dump(StringBuilder line1, StringBuilder line2) {
+ synchronized (ImageBlock.this) {
+// Log.v(TAG, "block " + mBlockNumber + " isVis == " + mIsVisible);
+ line2.append(mCompletedMask != 0xF ? 'L' : '_');
+ line1.append(mIsVisible ? 'V' : ' ');
+ }
+ }
+
+ ImageBlock() {
+ mPaint.setTextSize(14F);
+ mPaint.setStyle(Paint.Style.FILL);
+
+ mBlockNumber = -1;
+ mCellOutline = GridViewSpecial.this.getResources().getDrawable(android.R.drawable.gallery_thumb);
+ }
+
+ private void recycleBitmaps() {
+ synchronized (ImageBlock.this) {
+ mBitmap.recycle();
+ mBitmap = null;
+ }
+ }
+
+ private void cancelExistingRequests() {
+ synchronized (ImageBlock.this) {
+ for (int i = 0; i < mCurrentSpec.mColumns; i++) {
+ int mask = (1 << i);
+ if ((mRequestedMask & mask) != 0) {
+ int pos = (mBlockNumber * mCurrentSpec.mColumns) + i;
+ if (mLoader.cancel(mGallery.mAllImages.getImageAt(pos))) {
+ mRequestedMask &= ~mask;
+ }
+ }
+ }
+ }
+ }
+
+ private void setStart(final int blockNumber) {
+ synchronized (ImageBlock.this) {
+ if (blockNumber == mBlockNumber)
+ return;
+
+ cancelExistingRequests();
+
+ mBlockNumber = blockNumber;
+ mRequestedMask = 0;
+ mCompletedMask = 0;
+ mCanvas.drawColor(0xFF000000);
+ mPaint.setColor(0xFFDDDDDD);
+ int imageNumber = blockNumber * mCurrentSpec.mColumns;
+ int lastImageNumber = mGallery.mAllImages.getCount() - 1;
+
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+
+ final int yPos = spacing;
+
+ for (int col = 0; col < mCurrentSpec.mColumns; col++) {
+ if (imageNumber++ >= lastImageNumber)
+ break;
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+ mCanvas.drawRect(xPos, yPos, xPos+mCurrentSpec.mCellWidth, yPos+mCurrentSpec.mCellHeight, mPaint);
+ paintSel(0, xPos, yPos);
+ }
+ }
+ }
+
+ private boolean setVisibility(boolean isVis) {
+ synchronized (ImageBlock.this) {
+ boolean retval = mIsVisible != isVis;
+ mIsVisible = isVis;
+ return retval;
+ }
+ }
+
+ private int startLoading() {
+ synchronized (ImageBlock.this) {
+ final int startRow = mBlockNumber;
+ int count = mGallery.mAllImages.getCount();
+
+ if (startRow == -1)
+ return 0;
+
+ if ((startRow * mCurrentSpec.mColumns) >= count) {
+ return 0;
+ }
+
+ int retVal = 0;
+ int base = (mBlockNumber * mCurrentSpec.mColumns);
+ for (int col = 0; col < mCurrentSpec.mColumns; col++) {
+ if ((mCompletedMask & (1 << col)) != 0) {
+ continue;
+ }
+
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+ final int yPos = spacing;
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+
+ int pos = base + col;
+ if (pos >= count)
+ break;
+
+ ImageManager.IImage image = mGallery.mAllImages.getImageAt(pos);
+ if (image != null) {
+// Log.v(TAG, "calling loadImage " + (base + col));
+ loadImage(base, col, image, xPos, yPos);
+ retVal += 1;
+ }
+ }
+ return retVal;
+
+ }}
+
+ Bitmap resizeBitmap(Bitmap b) {
+ // assume they're both square for now
+ if (b == null || (b.getWidth() == mCurrentSpec.mCellWidth && b.getHeight() == mCurrentSpec.mCellHeight)) {
+ return b;
+ }
+ float scale = (float) mCurrentSpec.mCellWidth / (float)b.getWidth();
+ Matrix m = new Matrix();
+ m.setScale(scale, scale, b.getWidth(), b.getHeight());
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false);
+ return b2;
+ }
+
+ private void drawBitmap(ImageManager.IImage image, int base, int baseOffset, Bitmap b, int xPos, int yPos) {
+ mCanvas.setBitmap(mBitmap);
+ if (b != null) {
+ // if the image is close to the target size then crop, otherwise scale
+ // both the bitmap and the view should be square but I suppose that could
+ // change in the future.
+ int w = mCurrentSpec.mCellWidth;
+ int h = mCurrentSpec.mCellHeight;
+
+ int bw = b.getWidth();
+ int bh = b.getHeight();
+
+ int deltaW = bw - w;
+ int deltaH = bh - h;
+
+ if (deltaW < 10 && deltaH < 10) {
+ int halfDeltaW = deltaW / 2;
+ int halfDeltaH = deltaH / 2;
+ android.graphics.Rect src = new android.graphics.Rect(0+halfDeltaW, 0+halfDeltaH, bw-halfDeltaW, bh-halfDeltaH);
+ android.graphics.Rect dst = new android.graphics.Rect(xPos, yPos, xPos+w, yPos+h);
+ if (src.width() != dst.width() || src.height() != dst.height()) {
+ if (Config.LOGV){
+ Log.v(TAG, "nope... width doesn't match " + src.width() + " " + dst.width());
+ Log.v(TAG, "nope... height doesn't match " + src.height() + " " + dst.height());
+ }
+ }
+ mCanvas.drawBitmap(b, src, dst, mPaint);
+ } else {
+ android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh);
+ android.graphics.Rect dst = new android.graphics.Rect(xPos, yPos, xPos+w, yPos+h);
+ mCanvas.drawBitmap(b, src, dst, mPaint);
+ }
+ } else {
+ // If the thumbnail cannot be drawn, put up an error icon instead
+ Bitmap error = mImageBlockManager.getErrorBitmap();
+ int width = error.getWidth();
+ int height = error.getHeight();
+ Rect source = new Rect(0, 0, width, height);
+ int left = (mCurrentSpec.mCellWidth - width) / 2 + xPos;
+ int top = (mCurrentSpec.mCellHeight - height) / 2 + yPos;
+ Rect dest = new Rect(left, top, left + width, top + height);
+ mCanvas.drawBitmap(error, source, dest, mPaint);
+ }
+ paintSel(base + baseOffset, xPos, yPos);
+ }
+
+ private void repaintSelection() {
+ int count = mGallery.mAllImages.getCount();
+ int startPos = mBlockNumber * mCurrentSpec.mColumns;
+ synchronized (ImageBlock.this) {
+ for (int i = 0; i < mCurrentSpec.mColumns; i++) {
+ int pos = startPos + i;
+
+ if (pos >= count)
+ break;
+
+ int row = 0; // i / mCurrentSpec.mColumns;
+ int col = i - (row * mCurrentSpec.mColumns);
+
+ // this is duplicated from getOrKick (TODO: don't duplicate this code)
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+ final int yPos = spacing + (row * (mCurrentSpec.mCellHeight + spacing));
+ final int xPos = leftSpacing + (col * (mCurrentSpec.mCellWidth + spacing));
+
+ paintSel(pos, xPos, yPos);
+ }
+ }
+ }
+
+ private void paintSel(int pos, int xPos, int yPos) {
+ if (pos == mCurrentSelection && mShowSelection) {
+ mCellOutline.setState(ENABLED_FOCUSED_SELECTED_WINDOW_FOCUSED_STATE_SET);
+ } else {
+ mCellOutline.setState(EMPTY_STATE_SET);
+ }
+ mCanvas.setBitmap(mBitmap);
+ mCellOutline.setBounds(xPos, yPos, xPos+mCurrentSpec.mCellWidth, yPos+mCurrentSpec.mCellHeight);
+ mCellOutline.draw(mCanvas);
+ }
+
+ private void loadImage(
+ final int base,
+ final int baseOffset,
+ final ImageManager.IImage image,
+ final int xPos,
+ final int yPos) {
+ synchronized (ImageBlock.this) {
+ final int startBlock = mBlockNumber;
+ final int pos = base + baseOffset;
+ final ImageLoader.LoadedCallback r = new ImageLoader.LoadedCallback() {
+ public void run(Bitmap b) {
+ boolean more = false;
+ synchronized (ImageBlock.this) {
+ if (startBlock != mBlockNumber) {
+// Log.v(TAG, "wanted block " + mBlockNumber + " but got " + startBlock);
+ return;
+ }
+
+ if (mBitmap == null) {
+ return;
+ }
+
+ drawBitmap(image, base, baseOffset, b, xPos, yPos);
+
+ int mask = (1 << baseOffset);
+ mRequestedMask &= ~mask;
+ mCompletedMask |= mask;
+
+ // Log.v(TAG, "for " + mBlockNumber + " mRequestedMask is " + String.format("%x", mRequestedMask) + " and mCompletedMask is " + String.format("%x", mCompletedMask));
+
+ if (mRequestedMask == 0) {
+ if (mIsVisible) {
+ postInvalidate();
+ }
+ more = true;
+ }
+ }
+ if (b != null)
+ b.recycle();
+
+ if (more) {
+ synchronized (ImageBlockManager.this) {
+ ImageBlockManager.this.notify();
+ mWorkCounter += 1;
+ }
+ }
+ if (sDump)
+ ImageBlockManager.this.dump();
+ }
+ };
+ mRequestedMask |= (1 << baseOffset);
+ mLoader.getBitmap(image, pos, r, mIsVisible, false);
+ }
+ }
+ }
+ }
+
+ public void init(Handler handler) {
+ mHandler = handler;
+ }
+
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (false) {
+ canvas.drawRect(0, 0, getWidth(), getHeight(), mGridViewPaint);
+ if (Config.LOGV)
+ Log.v(TAG, "painting background w/h " + getWidth() + " / " + getHeight());
+ return;
+ }
+
+ if (mImageBlockManager != null) {
+ mImageBlockManager.doDraw(canvas);
+ mImageBlockManager.moveDataWindow(mDirectionBiasDown, false);
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScroller != null) {
+ boolean more = mScroller.computeScrollOffset();
+ scrollTo(0, (int)mScroller.getCurrY());
+ if (more) {
+ postInvalidate(); // So we draw again
+ } else {
+ mScroller = null;
+ }
+ } else {
+ super.computeScroll();
+ }
+ }
+
+ private android.graphics.Rect getRectForPosition(int pos) {
+ int row = pos / mCurrentSpec.mColumns;
+ int col = pos - (row * mCurrentSpec.mColumns);
+
+ int left = mCurrentSpec.mLeftEdgePadding + (col * mCurrentSpec.mCellWidth) + (Math.max(0, col-1) * mCurrentSpec.mCellSpacing);
+ int top = (row * mCurrentSpec.mCellHeight) + (row * mCurrentSpec.mCellSpacing);
+
+ return new android.graphics.Rect(left, top, left + mCurrentSpec.mCellWidth + mCurrentSpec.mCellWidth, top + mCurrentSpec.mCellHeight + mCurrentSpec.mCellSpacing);
+ }
+
+ int computeSelectedIndex(android.view.MotionEvent ev) {
+ int spacing = mCurrentSpec.mCellSpacing;
+ int leftSpacing = mCurrentSpec.mLeftEdgePadding;
+
+ int x = (int) ev.getX();
+ int y = (int) ev.getY();
+ int row = (mScrollY + y - spacing) / (mCurrentSpec.mCellHeight + spacing);
+ int col = Math.min(mCurrentSpec.mColumns - 1, (x - leftSpacing) / (mCurrentSpec.mCellWidth + spacing));
+ return (row * mCurrentSpec.mColumns) + col;
+ }
+
+ @Override
+ public boolean onTouchEvent(android.view.MotionEvent ev) {
+ mGestureDetector.onTouchEvent(ev);
+ return true;
+ }
+
+ private void onSelect(int index) {
+ if (index >= 0 && index < mGallery.mAllImages.getCount()) {
+ ImageManager.IImage img = mGallery.mAllImages.getImageAt(index);
+ if (img == null)
+ return;
+
+ if (mGallery.isPickIntent()) {
+ mGallery.launchCropperOrFinish(img);
+ } else {
+ Uri targetUri = img.fullSizeImageUri();
+ Uri thisUri = mGallery.getIntent().getData();
+ if (thisUri != null) {
+ String bucket = thisUri.getQueryParameter("bucketId");
+ if (bucket != null) {
+ targetUri = targetUri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+ }
+ }
+ Intent intent = new Intent(Intent.ACTION_VIEW, targetUri);
+
+ // this should be unnecessary but if you remove this line then executing
+ // the subsequent startActivity causes the user to have to choose among
+ // ViewImage and a number of bogus entries (like attaching the image to
+ // a contact).
+ intent.setClass(mContext, ViewImage.class);
+ try {
+ mContext.startActivity(intent);
+ } catch (Exception ex) {
+ // sdcard removal??
+ }
+ }
+ }
+ }
+
+ @Override
+ public void scrollBy(int x, int y) {
+ scrollTo(x, mScrollY + y);
+ }
+
+ Toast mDateLocationToast;
+ int [] mDateRange = new int[2];
+
+ private String month(int month) {
+ String text = "";
+ switch (month) {
+ case 0: text = "January"; break;
+ case 1: text = "February"; break;
+ case 2: text = "March"; break;
+ case 3: text = "April"; break;
+ case 4: text = "May"; break;
+ case 5: text = "June"; break;
+ case 6: text = "July"; break;
+ case 7: text = "August"; break;
+ case 8: text = "September"; break;
+ case 9: text = "October"; break;
+ case 10: text = "November"; break;
+ case 11: text = "December"; break;
+ }
+ return text;
+ }
+
+ Runnable mToastRunnable = new Runnable() {
+ public void run() {
+ if (mDateLocationToast != null) {
+ mDateLocationToast.cancel();
+ mDateLocationToast = null;
+ }
+
+ int count = mGallery.mAllImages.getCount();
+ if (count == 0)
+ return;
+
+ GridViewSpecial.this.mImageBlockManager.getVisibleRange(mDateRange);
+
+ ImageManager.IImage firstImage = mGallery.mAllImages.getImageAt(mDateRange[0]);
+ int lastOffset = Math.min(count-1, mDateRange[1]);
+ ImageManager.IImage lastImage = mGallery.mAllImages.getImageAt(lastOffset);
+
+ GregorianCalendar dateStart = new GregorianCalendar();
+ GregorianCalendar dateEnd = new GregorianCalendar();
+
+ dateStart.setTimeInMillis(firstImage.getDateTaken());
+ dateEnd.setTimeInMillis(lastImage.getDateTaken());
+
+ String text1 = month(dateStart.get(Calendar.MONTH)) + " " + dateStart.get(Calendar.YEAR);
+ String text2 = month(dateEnd .get(Calendar.MONTH)) + " " + dateEnd .get(Calendar.YEAR);
+
+ String text = text1;
+ if (!text2.equals(text1))
+ text = text + " : " + text2;
+
+ mDateLocationToast = Toast.makeText(mContext, text, Toast.LENGTH_LONG);
+ mDateLocationToast.show();
+ }
+ };
+
+ @Override
+ public void scrollTo(int x, int y) {
+ y = Math.min(mMaxScrollY, y);
+ y = Math.max(mMinScrollY, y);
+ if (y > mScrollY)
+ mDirectionBiasDown = true;
+ else if (y < mScrollY)
+ mDirectionBiasDown = false;
+ super.scrollTo(x, y);
+ }
+ }
+}
diff --git a/src/com/android/camera/ImageLoader.java b/src/com/android/camera/ImageLoader.java
new file mode 100644
index 0000000..f3e04d7
--- /dev/null
+++ b/src/com/android/camera/ImageLoader.java
@@ -0,0 +1,348 @@
+/*
+ * 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 android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.util.Config;
+import android.util.Log;
+
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class ImageLoader {
+ private static final String TAG = "ImageLoader";
+
+ // queue of work to do in the workder thread
+ private ArrayList<WorkItem> mQueue = new ArrayList<WorkItem>();
+ private ArrayList<WorkItem> mInProgress = new ArrayList<WorkItem>();
+
+ // array of image id's that have bad thumbnails
+ private ArrayList<Uri> mBadThumbnailList = new ArrayList<Uri>();
+
+ // the worker thread and a done flag so we know when to exit
+ // currently we only exit from finalize
+ private boolean mDone;
+ private ArrayList<Thread> mDecodeThreads = new ArrayList<Thread>();
+ private android.os.Handler mHandler;
+
+ private int mThreadCount = 1;
+
+ synchronized void clear(Uri uri) {
+ }
+
+ synchronized public void dump() {
+ synchronized (mQueue) {
+ if (Config.LOGV)
+ Log.v(TAG, "Loader queue length is " + mQueue.size());
+ }
+ }
+
+ public interface LoadedCallback {
+ public void run(Bitmap result);
+ }
+
+ public void pushToFront(final ImageManager.IImage image) {
+ synchronized (mQueue) {
+ WorkItem w = new WorkItem(image, 0, null, false);
+
+ int existing = mQueue.indexOf(w);
+ if (existing >= 1) {
+ WorkItem existingWorkItem = mQueue.remove(existing);
+ mQueue.add(0, existingWorkItem);
+ mQueue.notifyAll();
+ }
+ }
+ }
+
+ public boolean cancel(final ImageManager.IImage image) {
+ synchronized (mQueue) {
+ WorkItem w = new WorkItem(image, 0, null, false);
+
+ int existing = mQueue.indexOf(w);
+ if (existing >= 0) {
+ mQueue.remove(existing);
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public Bitmap getBitmap(final ImageManager.IImage image, final LoadedCallback imageLoadedRunnable, final boolean postAtFront, boolean postBack) {
+ return getBitmap(image, 0, imageLoadedRunnable, postAtFront, postBack);
+ }
+
+ public Bitmap getBitmap(final ImageManager.IImage image, int tag, final LoadedCallback imageLoadedRunnable, final boolean postAtFront, boolean postBack) {
+ synchronized (mDecodeThreads) {
+ if (mDecodeThreads.size() == 0) {
+ start();
+ }
+ }
+ long t1 = System.currentTimeMillis();
+ long t2,t3,t4;
+ synchronized (mQueue) {
+ t2 = System.currentTimeMillis();
+ WorkItem w = new WorkItem(image, tag, imageLoadedRunnable, postBack);
+
+ if (!mInProgress.contains(w)) {
+ boolean contains = mQueue.contains(w);
+ if (contains) {
+ if (postAtFront) {
+ // move this item to the front
+ mQueue.remove(w);
+ mQueue.add(0, w);
+ }
+ } else {
+ if (postAtFront)
+ mQueue.add(0, w);
+ else
+ mQueue.add(w);
+ mQueue.notifyAll();
+ }
+ }
+ if (false)
+ dumpQueue("+" + (postAtFront ? "F " : "B ") + tag + ": ");
+ t3 = System.currentTimeMillis();
+ }
+ t4 = System.currentTimeMillis();
+// Log.v(TAG, "getBitmap breakdown: tot= " + (t4-t1) + "; " + "; " + (t4-t3) + "; " + (t3-t2) + "; " + (t2-t1));
+ return null;
+ }
+
+ private void dumpQueue(String s) {
+ synchronized (mQueue) {
+ StringBuilder sb = new StringBuilder(s);
+ for (int i = 0; i < mQueue.size(); i++) {
+ sb.append(mQueue.get(i).mTag + " ");
+ }
+ if (Config.LOGV)
+ Log.v(TAG, sb.toString());
+ }
+ }
+
+ long bitmapSize(Bitmap b) {
+ return b.getWidth() * b.getHeight() * 4;
+ }
+
+ class WorkItem {
+ ImageManager.IImage mImage;
+ int mTargetX, mTargetY;
+ int mTag;
+ LoadedCallback mOnLoadedRunnable;
+ boolean mPostBack;
+
+ WorkItem(ImageManager.IImage image, int tag, LoadedCallback onLoadedRunnable, boolean postBack) {
+ mImage = image;
+ mTag = tag;
+ mOnLoadedRunnable = onLoadedRunnable;
+ mPostBack = postBack;
+ }
+
+ public boolean equals(Object other) {
+ WorkItem otherWorkItem = (WorkItem) other;
+ if (otherWorkItem.mImage != mImage)
+ return false;
+
+ return true;
+ }
+
+ public int hashCode() {
+ return mImage.fullSizeImageUri().hashCode();
+ }
+ }
+
+ public ImageLoader(android.os.Handler handler, int threadCount) {
+ mThreadCount = threadCount;
+ mHandler = handler;
+ start();
+ }
+
+ synchronized private void start() {
+ if (Config.LOGV)
+ Log.v(TAG, "ImageLoader.start() <<<<<<<<<<<<<<<<<<<<<<<<<<<<");
+
+ synchronized (mDecodeThreads) {
+ if (mDecodeThreads.size() > 0)
+ return;
+
+ mDone = false;
+ for (int i = 0;i < mThreadCount; i++) {
+ Thread t = new Thread(new Runnable() {
+ // pick off items on the queue, one by one, and compute their bitmap.
+ // place the resulting bitmap in the cache. then post a notification
+ // back to the ui so things can get updated appropriately.
+ public void run() {
+ while (!mDone) {
+ WorkItem workItem = null;
+ synchronized (mQueue) {
+ if (mQueue.size() > 0) {
+ workItem = mQueue.remove(0);
+ mInProgress.add(workItem);
+ }
+ else {
+ try {
+ mQueue.wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ if (workItem != null) {
+ if (false)
+ dumpQueue("-" + workItem.mTag + ": ");
+ Bitmap b = null;
+ try {
+ b = workItem.mImage.miniThumbBitmap();
+ } catch (Exception ex) {
+ Log.e(TAG, "couldn't load miniThumbBitmap " + ex.toString());
+ // sd card removal??
+ }
+ if (b == null) {
+ if (Config.LOGV) Log.v(TAG, "unable to read thumbnail for " + workItem.mImage.fullSizeImageUri());
+ mBadThumbnailList.add(workItem.mImage.fullSizeImageUri());
+ }
+
+ synchronized (mQueue) {
+ mInProgress.remove(workItem);
+ }
+
+ if (workItem.mOnLoadedRunnable != null) {
+ if (workItem.mPostBack) {
+ final WorkItem w1 = workItem;
+ final Bitmap bitmap = b;
+ if (!mDone) {
+ mHandler.post(new Runnable() {
+ public void run() {
+ w1.mOnLoadedRunnable.run(bitmap);
+ }
+ });
+ }
+ } else {
+ workItem.mOnLoadedRunnable.run(b);
+ }
+ }
+ }
+ }
+ }
+ });
+ t.setName("image-loader-" + i);
+ mDecodeThreads.add(t);
+ t.start();
+ }
+ }
+ }
+
+ public static Bitmap transform(Matrix scaler, Bitmap source, int targetWidth, int targetHeight,
+ boolean scaleUp) {
+ int deltaX = source.getWidth() - targetWidth;
+ int deltaY = source.getHeight() - targetHeight;
+ if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
+ /*
+ * In this case the bitmap is smaller, at least in one dimension, than the
+ * target. Transform it by placing as much of the image as possible into
+ * the target and leaving the top/bottom or left/right (or both) black.
+ */
+ Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(b2);
+
+ int deltaXHalf = Math.max(0, deltaX/2);
+ int deltaYHalf = Math.max(0, deltaY/2);
+ Rect src = new Rect(
+ deltaXHalf,
+ deltaYHalf,
+ deltaXHalf + Math.min(targetWidth, source.getWidth()),
+ deltaYHalf + Math.min(targetHeight, source.getHeight()));
+ int dstX = (targetWidth - src.width()) / 2;
+ int dstY = (targetHeight - src.height()) / 2;
+ Rect dst = new Rect(
+ dstX,
+ dstY,
+ targetWidth - dstX,
+ targetHeight - dstY);
+ if (Config.LOGV)
+ Log.v(TAG, "draw " + src.toString() + " ==> " + dst.toString());
+ c.drawBitmap(source, src, dst, null);
+ return b2;
+ }
+ float bitmapWidthF = source.getWidth();
+ float bitmapHeightF = source.getHeight();
+
+ float bitmapAspect = bitmapWidthF / bitmapHeightF;
+ float viewAspect = (float) targetWidth / (float) targetHeight;
+
+ if (bitmapAspect > viewAspect) {
+ float scale = targetHeight / bitmapHeightF;
+ if (scale < .9F || scale > 1F) {
+ scaler.setScale(scale, scale);
+ } else {
+ scaler = null;
+ }
+ } else {
+ float scale = targetWidth / bitmapWidthF;
+ if (scale < .9F || scale > 1F) {
+ scaler.setScale(scale, scale);
+ } else {
+ scaler = null;
+ }
+ }
+
+ Bitmap b1;
+ if (scaler != null) {
+ // this is used for minithumb and crop, so we want to filter here.
+ b1 = Bitmap.createBitmap(source, 0, 0,
+ source.getWidth(), source.getHeight(), scaler, true);
+ } else {
+ b1 = source;
+ }
+
+ int dx1 = Math.max(0, b1.getWidth() - targetWidth);
+ int dy1 = Math.max(0, b1.getHeight() - targetHeight);
+
+ Bitmap b2 = Bitmap.createBitmap(
+ b1,
+ dx1/2,
+ dy1/2,
+ targetWidth,
+ targetHeight);
+
+ if (b1 != source)
+ b1.recycle();
+
+ return b2;
+ }
+
+ public void stop() {
+ if (Config.LOGV)
+ Log.v(TAG, "ImageLoader.stop " + mDecodeThreads.size() + " threads");
+ mDone = true;
+ synchronized (mQueue) {
+ mQueue.notifyAll();
+ }
+ while (mDecodeThreads.size() > 0) {
+ Thread t = mDecodeThreads.get(0);
+ try {
+ t.join();
+ mDecodeThreads.remove(0);
+ } catch (InterruptedException ex) {
+ // so now what?
+ }
+ }
+ }
+}
diff --git a/src/com/android/camera/ImageManager.java b/src/com/android/camera/ImageManager.java
new file mode 100755
index 0000000..8d3f90a
--- /dev/null
+++ b/src/com/android/camera/ImageManager.java
@@ -0,0 +1,3632 @@
+/*
+ * 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 android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.ContentUris;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Matrix;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.provider.BaseColumns;
+import android.provider.DrmStore;
+import android.provider.MediaStore;
+import android.provider.MediaStore.Images.ImageColumns;
+import android.provider.MediaStore.Images.Thumbnails;
+import android.provider.MediaStore.Images;
+import android.provider.MediaStore.MediaColumns;
+import android.util.Config;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.RandomAccessFile;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ *
+ * ImageManager is used to retrieve and store images
+ * in the media content provider.
+ *
+ */
+public class ImageManager {
+ public static final String CAMERA_IMAGE_BUCKET_NAME = "/sdcard/dcim/camera";
+ public static final String CAMERA_IMAGE_BUCKET_ID = String.valueOf(CAMERA_IMAGE_BUCKET_NAME.hashCode());
+
+ // To enable verbose logging for this class, change false to true. The other logic ensures that
+ // this logging can be disabled by turned off DEBUG and lower, and that it can be enabled by
+ // "setprop log.tag.ImageManager VERBOSE" if desired.
+ //
+ // IMPORTANT: Never check in this file set to true!
+ private static final boolean VERBOSE = Config.LOGD && (false || Config.LOGV);
+ private static final String TAG = "ImageManager";
+
+ private static final int MINI_THUMB_DATA_FILE_VERSION = 3;
+
+ static public void debug_where(String tag, String msg) {
+ try {
+ throw new Exception();
+ } catch (Exception ex) {
+ if (msg != null) {
+ Log.v(tag, msg);
+ }
+ boolean first = true;
+ for (StackTraceElement s : ex.getStackTrace()) {
+ if (first)
+ first = false;
+ else
+ Log.v(tag, s.toString());
+ }
+ }
+ }
+
+ /*
+ * Compute the sample size as a function of the image size and the target.
+ * Scale the image down so that both the width and height are just above
+ * the target. If this means that one of the dimension goes from above
+ * the target to below the target (e.g. given a width of 480 and an image
+ * width of 600 but sample size of 2 -- i.e. new width 300 -- bump the
+ * sample size down by 1.
+ */
+ private static int computeSampleSize(BitmapFactory.Options options, int target) {
+ int w = options.outWidth;
+ int h = options.outHeight;
+
+ int candidateW = w / target;
+ int candidateH = h / target;
+ int candidate = Math.max(candidateW, candidateH);
+
+ if (candidate == 0)
+ return 1;
+
+ if (candidate > 1) {
+ if ((w > target) && (w / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (candidate > 1) {
+ if ((h > target) && (h / candidate) < target)
+ candidate -= 1;
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, "for w/h " + w + "/" + h + " returning " + candidate + "(" + (w/candidate) + " / " + (h/candidate));
+
+ return candidate;
+ }
+ /*
+ * All implementors of ICancelable should inherit from BaseCancelable
+ * since it provides some convenience methods such as acknowledgeCancel
+ * and checkCancel.
+ */
+ public abstract class BaseCancelable implements ICancelable {
+ boolean mCancel = false;
+ boolean mFinished = false;
+
+ /*
+ * Subclasses should call acknowledgeCancel when they're finished with
+ * their operation.
+ */
+ protected void acknowledgeCancel() {
+ synchronized (this) {
+ mFinished = true;
+ if (!mCancel)
+ return;
+ if (mCancel) {
+ this.notify();
+ }
+ }
+ }
+
+ public boolean cancel() {
+ synchronized (this) {
+ if (mCancel) {
+ return false;
+ }
+ if (mFinished) {
+ return false;
+ }
+ mCancel = true;
+ boolean retVal = doCancelWork();
+
+ try {
+ this.wait();
+ } catch (InterruptedException ex) {
+ // now what??? TODO
+ }
+
+ return retVal;
+ }
+ }
+
+ /*
+ * Subclasses can call this to see if they have been canceled.
+ * This is the polling model.
+ */
+ protected void checkCanceled() throws CanceledException {
+ synchronized (this) {
+ if (mCancel)
+ throw new CanceledException();
+ }
+ }
+
+ /*
+ * Subclasses implement this method to take whatever action
+ * is necessary when getting canceled. Sometimes it's not
+ * possible to do anything in which case the "checkCanceled"
+ * polling model may be used (or some combination).
+ */
+ public abstract boolean doCancelWork();
+ }
+
+ private static final int sBytesPerMiniThumb = 10000;
+ static final private byte [] sMiniThumbData = new byte[sBytesPerMiniThumb];
+
+ /**
+ * Represents a particular image and provides access
+ * to the underlying bitmap and two thumbnail bitmaps
+ * as well as other information such as the id, and
+ * the path to the actual image data.
+ */
+ abstract class BaseImage implements IImage {
+ protected ContentResolver mContentResolver;
+ protected long mId, mMiniThumbMagic;
+ protected BaseImageList mContainer;
+ protected HashMap<String, String> mExifData;
+ protected int mCursorRow;
+
+ protected BaseImage(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow) {
+ mContentResolver = cr;
+ mId = id;
+ mMiniThumbMagic = miniThumbId;
+ mContainer = container;
+ mCursorRow = cursorRow;
+ }
+
+ abstract Bitmap.CompressFormat compressionType();
+
+ public void commitChanges() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.commitUpdates();
+ c.requery();
+ }
+ }
+ }
+
+ /**
+ * Take a given bitmap and compress it to a file as described
+ * by the Uri parameter.
+ *
+ * @param bitmap the bitmap to be compressed/stored
+ * @param uri where to store the bitmap
+ * @return true if we succeeded
+ */
+ protected IGetBoolean_cancelable compressImageToFile(
+ final Bitmap bitmap,
+ final byte [] jpegData,
+ final Uri uri) {
+ class CompressImageToFile extends BaseCancelable implements IGetBoolean_cancelable {
+ ThreadSafeOutputStream mOutputStream = null;
+
+ public boolean doCancelWork() {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ return true;
+ } catch (IOException ex) {
+ // TODO what to do here
+ }
+ }
+ return false;
+ }
+
+ public boolean get() {
+ try {
+ long t1 = System.currentTimeMillis();
+ OutputStream delegate = mContentResolver.openOutputStream(uri);
+ synchronized (this) {
+ checkCanceled();
+ mOutputStream = new ThreadSafeOutputStream(delegate);
+ }
+ long t2 = System.currentTimeMillis();
+ if (bitmap != null) {
+ bitmap.compress(compressionType(), 75, mOutputStream);
+ } else {
+ long x1 = System.currentTimeMillis();
+ mOutputStream.write(jpegData);
+ long x2 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "done writing... " + jpegData.length + " bytes took " + (x2-x1));
+ }
+ long t3 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, String.format("CompressImageToFile.get took %d (%d, %d)",(t3-t1),(t2-t1),(t3-t2)));
+ return true;
+ } catch (FileNotFoundException ex) {
+ return false;
+ } catch (CanceledException ex) {
+ return false;
+ } catch (IOException ex) {
+ return false;
+ }
+ finally {
+ if (mOutputStream != null) {
+ try {
+ mOutputStream.close();
+ } catch (IOException ex) {
+ // not much we can do here so ignore
+ }
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new CompressImageToFile();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (!(other instanceof Image))
+ return false;
+
+ return fullSizeImageUri().equals(((Image)other).fullSizeImageUri());
+ }
+
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ return fullSizeBitmap(targetWidthHeight, true);
+ }
+
+ protected Bitmap fullSizeBitmap(int targetWidthHeight, boolean rotateAsNeeded) {
+ Uri url = mContainer.contentUri(mId);
+ if (VERBOSE) Log.v(TAG, "getCreateBitmap for " + url);
+ if (url == null)
+ return null;
+
+ Bitmap b = null;
+ if (b == null) {
+ b = makeBitmap(targetWidthHeight, url);
+ if (b != null && rotateAsNeeded) {
+ b = rotate(b, getDegreesRotated());
+ }
+ }
+ return b;
+ }
+
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor mPFD;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfdInput) {
+ mPFD = pfdInput;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthHeight, fullSizeImageUri(), mPFD, mOptions);
+ if (mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancelation of bitmap load success==" + (b == null ? "TRUE" : "FALSE") + " -- took " + (System.currentTimeMillis() - mCancelInitiationTime));
+ }
+ if (b != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ if (b != b2)
+ b.recycle();
+ b = b2;
+ }
+ }
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = mContentResolver.openFileDescriptor(fullSizeImageUri(), "r");
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (FileNotFoundException ex) {
+ return null;
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ public InputStream fullSizeImageData() {
+ try {
+ InputStream input = mContentResolver.openInputStream(
+ fullSizeImageUri());
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ public long fullSizeImageId() {
+ return mId;
+ }
+
+ public Uri fullSizeImageUri() {
+ return mContainer.contentUri(mId);
+ }
+
+ public IImageList getContainer() {
+ return mContainer;
+ }
+
+ Cursor getCursor() {
+ return mContainer.getCursor();
+ }
+
+ public long getDateTaken() {
+ if (mContainer.indexDateTaken() < 0) return 0;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getLong(mContainer.indexDateTaken());
+ }
+ }
+
+ protected int getDegreesRotated() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ if (mContainer.indexMimeType() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.MIME_TYPE },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String mimeType = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ mimeType = c.getString(mContainer.indexMimeType());
+ }
+ }
+ return mimeType;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDescription()
+ */
+ public String getDescription() {
+ if (mContainer.indexDescription() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DESCRIPTION },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ } else {
+ return "";
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String description = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ description = c.getString(mContainer.indexDescription());
+ }
+ }
+ return description;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getIsPrivate()
+ */
+ public boolean getIsPrivate() {
+ if (mContainer.indexPrivate() < 0) return false;
+ boolean isPrivate = false;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ isPrivate = c.getInt(mContainer.indexPrivate()) != 0;
+ }
+ }
+ return isPrivate;
+ }
+
+ public double getLatitude() {
+ if (mContainer.indexLatitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLatitude());
+ }
+ }
+
+ public double getLongitude() {
+ if (mContainer.indexLongitude() < 0) return 0D;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return c.getDouble(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getTitle()
+ */
+ public String getTitle() {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ if (mContainer.indexTitle() != -1) {
+ name = c.getString(mContainer.indexTitle());
+ }
+ }
+ }
+ return name != null && name.length() > 0 ? name : String.valueOf(mId);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#getDisplayName()
+ */
+ public String getDisplayName() {
+ if (mContainer.indexDisplayName() < 0) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.DISPLAY_NAME },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ return c.getString(1);
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ } else {
+ String name = null;
+ Cursor c = getCursor();
+ synchronized(c) {
+ if (c.moveToPosition(getRow())) {
+ name = c.getString(mContainer.indexDisplayName());
+ }
+ }
+ if (name != null && name.length() > 0)
+ return name;
+ }
+ return String.valueOf(mId);
+ }
+
+ public String getPicasaId() {
+ /*
+ if (mContainer.indexPicasaWeb() < 0) return null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveTo(getRow());
+ return c.getString(mContainer.indexPicasaWeb());
+ }
+ */
+ return null;
+ }
+
+ public int getRow() {
+ return mCursorRow;
+ }
+
+ public int getWidth() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outWidth;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public int getHeight() {
+ ParcelFileDescriptor input = null;
+ try {
+ Uri uri = fullSizeImageUri();
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options.outHeight;
+ } catch (IOException ex) {
+ return 0;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ public boolean hasLatLong() {
+ if (mContainer.indexLatitude() < 0 || mContainer.indexLongitude() < 0) return false;
+ Cursor c = getCursor();
+ synchronized (c) {
+ c.moveToPosition(getRow());
+ return !c.isNull(mContainer.indexLatitude()) && !c.isNull(mContainer.indexLongitude());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#imageId()
+ */
+ public long imageId() {
+ return mId;
+ }
+
+ /**
+ * Make a bitmap from a given Uri.
+ *
+ * @param uri
+ */
+ private Bitmap makeBitmap(int targetWidthOrHeight, Uri uri) {
+ ParcelFileDescriptor input = null;
+ try {
+ input = mContentResolver.openFileDescriptor(uri, "r");
+ return makeBitmap(targetWidthOrHeight, uri, input, null);
+ } catch (IOException ex) {
+ return null;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ return mContainer.makeBitmap(targetWidthHeight, uri, pfdInput, options);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumb1()
+ */
+ public Bitmap miniThumbBitmap() {
+ try {
+ long id = mId;
+ long dbMagic = mMiniThumbMagic;
+ if (dbMagic == 0 || dbMagic == id) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ if (VERBOSE) Log.v(TAG, "after computing thumbnail dbMagic is " + dbMagic);
+ }
+
+ synchronized(sMiniThumbData) {
+ dbMagic = mMiniThumbMagic;
+ byte [] data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ if (data == null) {
+ dbMagic = ((BaseImageList)getContainer()).checkThumbnail(this, getCursor(), getRow());
+ data = mContainer.getMiniThumbFromFile(id, sMiniThumbData, dbMagic);
+ }
+ if (data == null) {
+ if (VERBOSE)
+ Log.v(TAG, "unable to get miniThumbBitmap, data is null");
+ }
+ if (data != null) {
+ Bitmap b = BitmapFactory.decodeByteArray(data, 0, data.length);
+ if (b == null) {
+ if (VERBOSE) {
+ Log.v(TAG, "couldn't decode byte array for mini thumb, length was " + data.length);
+ }
+ }
+ return b;
+ }
+ }
+ return null;
+ } catch (Exception ex) {
+ Log.e(TAG, "miniThumbBitmap got exception " + ex.toString());
+ for (StackTraceElement s : ex.getStackTrace())
+ Log.e(TAG, "... " + s.toString());
+ return null;
+ }
+ }
+
+ public void onRemove() {
+ mContainer.mCache.remove(mId);
+ }
+
+ protected void saveMiniThumb(Bitmap source) {
+ mContainer.saveMiniThumbToFile(source, fullSizeImageId(), 0);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setDescription(String description) {
+ if (mContainer.indexDescription() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexDescription(), description);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setIsPrivate()
+ */
+ public void setIsPrivate(boolean isPrivate) {
+ if (mContainer.indexPrivate() < 0) return;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateInt(mContainer.indexPrivate(), isPrivate ? 1 : 0);
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#setName()
+ */
+ public void setName(String name) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ c.updateString(mContainer.indexTitle(), name);
+ }
+ }
+ }
+
+ public void setPicasaId(String id) {
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ fullSizeImageUri(),
+ new String[] { "_id", Images.Media.PICASA_ID },
+ null,
+ null, null);
+ if (c != null && c.moveToFirst()) {
+ if (VERBOSE) {
+ Log.v(TAG, "storing picasaid " + id + " for " + fullSizeImageUri());
+ }
+ c.updateString(1, id);
+ c.commitUpdates();
+ if (VERBOSE) {
+ Log.v(TAG, "updated image with picasa id " + id);
+ }
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#thumbUri()
+ */
+ public Uri thumbUri() {
+ Uri uri = fullSizeImageUri();
+ // The value for the query parameter cannot be null :-(, so using a dummy "1"
+ uri = uri.buildUpon().appendQueryParameter("thumb", "1").build();
+ return uri;
+ }
+
+ @Override
+ public String toString() {
+ return fullSizeImageUri().toString();
+ }
+ }
+
+ abstract static class BaseImageList implements IImageList {
+ Context mContext;
+ ContentResolver mContentResolver;
+ Uri mBaseUri, mUri;
+ int mSort;
+ String mBucketId;
+ boolean mDistinct;
+ Cursor mCursor;
+ boolean mCursorDeactivated;
+ protected HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
+
+ IImageList.OnChange mListener = null;
+ Handler mHandler;
+ protected RandomAccessFile mMiniThumbData;
+ protected Uri mThumbUri;
+
+ public BaseImageList(Context ctx, ContentResolver cr, Uri uri, int sort, String bucketId) {
+ mContext = ctx;
+ mSort = sort;
+ mUri = uri;
+ mBaseUri = uri;
+
+ mContentResolver = cr;
+ }
+
+ String randomAccessFilePath(int version) {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim/.thumbnails";
+ String path = directoryName + "/.thumbdata" + version + "-" + mUri.hashCode();
+ return path;
+ }
+
+ RandomAccessFile miniThumbDataFile() {
+ if (mMiniThumbData == null) {
+ String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION);
+ File directory = new File(new File(path).getParent());
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ Log.e(TAG, "!!!! unable to create .thumbnails directory " + directory.toString());
+ }
+ }
+ File f = new File(path);
+ if (VERBOSE) Log.v(TAG, "file f is " + f.toString());
+ try {
+ mMiniThumbData = new RandomAccessFile(f, "rw");
+ } catch (IOException ex) {
+
+ }
+ }
+ return mMiniThumbData;
+ }
+
+ /**
+ * Store a given thumbnail in the database.
+ */
+ protected Bitmap storeThumbnail(Bitmap thumb, long imageId) {
+ if (thumb == null)
+ return null;
+
+ try {
+ Uri uri = getThumbnailUri(imageId, thumb.getWidth(), thumb.getHeight());
+ if (uri == null) {
+ return thumb;
+ }
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumb.compress(Bitmap.CompressFormat.JPEG, 60, thumbOut);
+ thumbOut.close();
+ return thumb;
+ }
+ catch (Exception ex) {
+ Log.d(TAG, "unable to store thumbnail: " + ex);
+ return thumb;
+ }
+ }
+
+ /**
+ * Store a JPEG thumbnail from the EXIF header in the database.
+ */
+ protected boolean storeThumbnail(byte[] jpegThumbnail, long imageId, int width, int height) {
+ if (jpegThumbnail == null)
+ return false;
+
+ Uri uri = getThumbnailUri(imageId, width, height);
+ if (uri == null) {
+ return false;
+ }
+ try {
+ OutputStream thumbOut = mContentResolver.openOutputStream(uri);
+ thumbOut.write(jpegThumbnail);
+ thumbOut.close();
+ return true;
+ }
+ catch (FileNotFoundException ex) {
+ return false;
+ }
+ catch (IOException ex) {
+ return false;
+ }
+ }
+
+ private Uri getThumbnailUri(long imageId, int width, int height) {
+ // we do not store thumbnails for DRM'd images
+ if (mThumbUri == null) {
+ return null;
+ }
+
+ Uri uri = null;
+ Cursor c = null;
+ try {
+ c = mContentResolver.query(
+ mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[]{String.valueOf(imageId)},
+ null);
+ if (c != null && c.moveToFirst()) {
+ // If, for some reaosn, we already have a row with a matching
+ // image id, then just update that row rather than creating a
+ // new row.
+ uri = ContentUris.withAppendedId(mThumbUri, c.getLong(indexThumbId()));
+ c.commitUpdates();
+ }
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ if (uri == null) {
+ ContentValues values = new ContentValues(4);
+ values.put(Images.Thumbnails.KIND, Images.Thumbnails.MINI_KIND);
+ values.put(Images.Thumbnails.IMAGE_ID, imageId);
+ values.put(Images.Thumbnails.HEIGHT, height);
+ values.put(Images.Thumbnails.WIDTH, width);
+ uri = mContentResolver.insert(mThumbUri, values);
+ }
+ return uri;
+ }
+
+ java.util.Random mRandom = new java.util.Random(System.currentTimeMillis());
+
+ protected SomewhatFairLock mLock = new SomewhatFairLock();
+
+ class SomewhatFairLock {
+ private Object mSync = new Object();
+ private boolean mLocked = false;
+ private ArrayList<Thread> mWaiting = new ArrayList<Thread>();
+
+ void lock() {
+// if (VERBOSE) Log.v(TAG, "lock... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ while (mLocked) {
+ try {
+// if (VERBOSE) Log.v(TAG, "waiting... thread " + Thread.currentThread().getId());
+ mWaiting.add(Thread.currentThread());
+ mSync.wait();
+ if (mWaiting.get(0) == Thread.currentThread()) {
+ mWaiting.remove(0);
+ break;
+ }
+ } catch (InterruptedException ex) {
+ //
+ }
+ }
+// if (VERBOSE) Log.v(TAG, "locked... thread " + Thread.currentThread().getId());
+ mLocked = true;
+ }
+ }
+
+ void unlock() {
+// if (VERBOSE) Log.v(TAG, "unlocking... thread " + Thread.currentThread().getId());
+ synchronized (mSync) {
+ mLocked = false;
+ mSync.notifyAll();
+ }
+ }
+ }
+
+ // If the photo has an EXIF thumbnail and it's big enough, extract it and save that JPEG as
+ // the large thumbnail without re-encoding it. We still have to decompress it though, in
+ // order to generate the minithumb.
+ private Bitmap createThumbnailFromEXIF(String filePath, long id) {
+ if (filePath != null) {
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (thumbData != null) {
+ // Sniff the size of the EXIF thumbnail before decoding it. Photos from the
+ // device will pass, but images that are side loaded from other cameras may not.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+ if (width >= THUMBNAIL_TARGET_SIZE && height >= THUMBNAIL_TARGET_SIZE) {
+ if (storeThumbnail(thumbData, id, width, height)) {
+ // this is used for *encoding* the minithumb, so
+ // we don't want to dither or convert to 565 here.
+ //
+ // Decode with a scaling factor
+ // to match MINI_THUMB_TARGET_SIZE closely
+ // which will produce much better scaling quality
+ // and is significantly faster.
+ options.inSampleSize = computeSampleSize(options, THUMBNAIL_TARGET_SIZE);
+
+ if (VERBOSE) {
+ Log.v(TAG, "in createThumbnailFromExif using inSampleSize of " + options.inSampleSize);
+ }
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length, options);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ // The fallback case is to decode the original photo to thumbnail size, then encode it as a
+ // JPEG. We return the thumbnail Bitmap in order to create the minithumb from it.
+ private Bitmap createThumbnailFromUri(Cursor c, long id) {
+ Uri uri = ContentUris.withAppendedId(mBaseUri, id);
+ Bitmap bitmap = makeBitmap(THUMBNAIL_TARGET_SIZE, uri, null, null);
+ if (bitmap != null) {
+ storeThumbnail(bitmap, id);
+ } else {
+ uri = ContentUris.withAppendedId(mBaseUri, id);
+ bitmap = makeBitmap(MINI_THUMB_TARGET_SIZE, uri, null, null);
+ }
+ return bitmap;
+ }
+
+ // returns id
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
+ long magic, fileMagic = 0, id;
+ try {
+ mLock.lock();
+ if (existingImage == null) {
+ // if we don't have an Image object then get the id and magic from
+ // the cursor. Synchonize on the cursor object.
+ synchronized (c) {
+ if (!c.moveToPosition(i)) {
+ return -1;
+ }
+ magic = c.getLong(indexMiniThumbId());
+ id = c.getLong(indexId());
+ }
+ } else {
+ // if we have an Image object then ask them for the magic/id
+ magic = existingImage.mMiniThumbMagic;
+ id = existingImage.fullSizeImageId();
+ }
+
+ if (magic != 0) {
+ // check the mini thumb file for the right data. Right is defined as
+ // having the right magic number at the offset reserved for this "id".
+ RandomAccessFile r = miniThumbDataFile();
+ if (r != null) {
+ synchronized (r) {
+ long pos = id * sBytesPerMiniThumb;
+ try {
+ // check that we can read the following 9 bytes (1 for the "status" and 8 for the long)
+ if (r.length() >= pos + 1 + 8) {
+ r.seek(pos);
+ if (r.readByte() == 1) {
+ fileMagic = r.readLong();
+ if (fileMagic == magic && magic != 0 && magic != id) {
+ return magic;
+ }
+ }
+ }
+ } catch (IOException ex) {
+ Log.v(TAG, "got exception checking file magic: " + ex);
+ }
+ }
+ }
+ if (VERBOSE) {
+ Log.v(TAG, "didn't verify... fileMagic: " + fileMagic + "; magic: " + magic + "; id: " + id + "; ");
+ }
+ }
+
+ // If we can't retrieve the thumbnail, first check if there is one embedded in the
+ // EXIF data. If not, or it's not big enough, decompress the full size image.
+ Bitmap bitmap = null;
+
+ String filePath = null;
+ synchronized (c) {
+ if (c.moveToPosition(i)) {
+ filePath = c.getString(indexData());
+ }
+ }
+
+ if (filePath != null) {
+ bitmap = createThumbnailFromEXIF(filePath, id);
+ if (bitmap == null) {
+ bitmap = createThumbnailFromUri(c, id);
+ }
+ synchronized (c) {
+ int degrees = 0;
+ if (c.moveToPosition(i)) {
+ int column = indexOrientation();
+ if (column >= 0)
+ degrees = c.getInt(column);
+ }
+ if (degrees != 0) {
+ Bitmap b2 = rotate(bitmap, degrees);
+ if (b2 != bitmap)
+ bitmap.recycle();
+ bitmap = b2;
+ }
+ }
+ }
+
+ // make a new magic number since things are out of sync
+ do {
+ magic = mRandom.nextLong();
+ } while (magic == 0);
+ if (bitmap != null) {
+ saveMiniThumbToFile(bitmap, id, magic);
+ bitmap.recycle();
+ }
+
+ synchronized (c) {
+ c.moveToPosition(i);
+ c.updateLong(indexMiniThumbId(), magic);
+ c.commitUpdates();
+ c.requery();
+ c.moveToPosition(i);
+
+ if (existingImage != null) {
+ existingImage.mMiniThumbMagic = magic;
+ }
+ return magic;
+ }
+ } finally {
+ mLock.unlock();
+ }
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ new String[] { "_id", "mini_thumb_magic" },
+ "mini_thumb_magic isnull and " + sWhereClause,
+ sAcceptableImageTypes,
+ "_id ASC");
+
+ int count = c.getCount();
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>> need to check " + c.getCount() + " rows");
+
+ c.close();
+
+ if (!ImageManager.hasStorage()) {
+ if (VERBOSE)
+ Log.v(TAG, "bailing from the image checker thread -- no storage");
+ return;
+ }
+
+ String oldPath = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION - 1);
+ File oldFile = new File(oldPath);
+
+ if (count == 0) {
+ // now check that we have the right thumbs file
+// Log.v(TAG, "count is zero but oldFile.exists() is " + oldFile.exists());
+ if (!oldFile.exists()) {
+ return;
+ }
+ }
+
+ c = getCursor();
+ try {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails found " + c.getCount());
+ int max = c.getCount();
+ int current = 0;
+ for (int i = 0; i < c.getCount(); i++) {
+ try {
+ checkThumbnail(null, c, i);
+ } catch (Exception ex) {
+ Log.e(TAG, "!!!!! failed to check thumbnail... was the sd card removed?");
+ break;
+ }
+ if (cb != null) {
+ if (!cb.checking(current, max)) {
+ if (VERBOSE) Log.v(TAG, "got false from checking... break <<<<<<<<<<<<<<<<<<<<<<<<");
+ break;
+ }
+ }
+ current += 1;
+ }
+ } finally {
+ if (VERBOSE) Log.v(TAG, "checkThumbnails existing after reaching count " + c.getCount());
+ try {
+ oldFile.delete();
+ } catch (Exception ex) {
+ // ignore
+ }
+ }
+ }
+
+ public void commitChanges() {
+ synchronized (mCursor) {
+ mCursor.commitUpdates();
+ requery();
+ }
+ }
+ protected Uri contentUri(long id) {
+ try {
+ // does our uri already have an id (single image query)?
+ // if so just return it
+ long existingId = ContentUris.parseId(mBaseUri);
+ if (existingId != id)
+ Log.e(TAG, "id mismatch");
+ return mBaseUri;
+ } catch (NumberFormatException ex) {
+ // otherwise tack on the id
+ return ContentUris.withAppendedId(mBaseUri, id);
+ }
+ }
+
+ public void deactivate() {
+ mCursorDeactivated = true;
+ mCursor.deactivate();
+ if (mMiniThumbData != null) {
+ try {
+ mMiniThumbData.close();
+ mMiniThumbData = null;
+ } catch (IOException ex) {
+
+ }
+ }
+ }
+
+ public void dump(String msg) {
+ int count = getCount();
+ if (VERBOSE) Log.v(TAG, "dump ImageList (count is " + count + ") " + msg);
+ for (int i = 0; i < count; i++) {
+ IImage img = getImageAt(i);
+ if (img == null)
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + "null");
+ else
+ if (VERBOSE) Log.v(TAG, " " + i + ": " + img.toString());
+ }
+ if (VERBOSE) Log.v(TAG, "end of dump container");
+ }
+ public int getCount() {
+ Cursor c = getCursor();
+ synchronized (c) {
+ try {
+ return c.getCount();
+ } catch (Exception ex) {
+ }
+ return 0;
+ }
+ }
+ protected Cursor getCursor() {
+ synchronized (mCursor) {
+ if (mCursorDeactivated) {
+ activateCursor();
+ }
+ return mCursor;
+ }
+ }
+
+ protected void activateCursor() {
+ requery();
+ }
+
+ public IImage getImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return null;
+ }
+ if (moved) {
+ try {
+ long id = c.getLong(0);
+ long miniThumbId = 0;
+ int rotation = 0;
+ if (indexMiniThumbId() != -1) {
+ miniThumbId = c.getLong(indexMiniThumbId());
+ }
+ if (indexOrientation() != -1) {
+ rotation = c.getInt(indexOrientation());
+ }
+ long timestamp = c.getLong(1);
+ IImage img = mCache.get(id);
+ if (img == null) {
+ img = make(id, miniThumbId, mContentResolver, this, timestamp, i, rotation);
+ mCache.put(id, img);
+ }
+ return img;
+ } catch (Exception ex) {
+ Log.e(TAG, "got this exception trying to create image object: " + ex);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "unable to moveTo to " + i + "; count is " + c.getCount());
+ return null;
+ }
+ }
+ }
+ public IImage getImageForUri(Uri uri) {
+ // TODO make this a hash lookup
+ for (int i = 0; i < getCount(); i++) {
+ if (getImageAt(i).fullSizeImageUri().equals(uri)) {
+ return getImageAt(i);
+ }
+ }
+ return null;
+ }
+ private byte [] getMiniThumbFromFile(long id, byte [] data, long magicCheck) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return null;
+
+ long pos = id * sBytesPerMiniThumb;
+ RandomAccessFile f = r;
+ synchronized (f) {
+ try {
+ f.seek(pos);
+ if (f.readByte() == 1) {
+ long magic = f.readLong();
+ if (magic != magicCheck) {
+ if (VERBOSE) Log.v(TAG, "for id " + id + "; magic: " + magic + "; magicCheck: " + magicCheck + " (fail)");
+ return null;
+ }
+ int length = f.readInt();
+ f.read(data, 0, length);
+ return data;
+ } else {
+ return null;
+ }
+ } catch (IOException ex) {
+ long fileLength;
+ try {
+ fileLength = f.length();
+ } catch (IOException ex1) {
+ fileLength = -1;
+ }
+ Log.e(TAG, "couldn't read thumbnail for " + id + "; " + ex.toString() + "; pos is " + pos + "; length is " + fileLength);
+ return null;
+ }
+ }
+ }
+ protected int getRowFor(IImage imageObj) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ int index = 0;
+ long targetId = imageObj.fullSizeImageId();
+ if (c.moveToFirst()) {
+ do {
+ if (c.getLong(0) == targetId) {
+ return index;
+ }
+ index += 1;
+ } while (c.moveToNext());
+ }
+ return -1;
+ }
+ }
+
+ protected abstract int indexOrientation();
+ protected abstract int indexDateTaken();
+ protected abstract int indexDescription();
+ protected abstract int indexMimeType();
+ protected abstract int indexData();
+ protected abstract int indexId();
+ protected abstract int indexLatitude();
+ protected abstract int indexLongitude();
+ protected abstract int indexMiniThumbId();
+ protected abstract int indexPicasaWeb();
+ protected abstract int indexPrivate();
+ protected abstract int indexTitle();
+ protected abstract int indexDisplayName();
+ protected abstract int indexThumbId();
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return null;
+ }
+
+ protected abstract Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options);
+
+ public boolean removeImage(IImage image) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ boolean moved;
+ try {
+ moved = c.moveToPosition(image.getRow());
+ } catch (Exception ex) {
+ Log.e(TAG, "removeImage got exception " + ex.toString());
+ return false;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ image.onRemove();
+ requery();
+ }
+ }
+ return false;
+ }
+
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImageList#removeImageAt(int)
+ */
+ public void removeImageAt(int i) {
+ Cursor c = getCursor();
+ synchronized (c) {
+ /*
+ * TODO: consider putting the image in a holding area so
+ * we can get it back as needed
+ * TODO: need to delete the thumbnails as well
+ */
+ dump("before delete");
+ IImage image = getImageAt(i);
+ boolean moved;
+ try {
+ moved = c.moveToPosition(i);
+ } catch (Exception ex) {
+ return;
+ }
+ if (moved) {
+ Uri u = image.fullSizeImageUri();
+ mContentResolver.delete(u, null, null);
+ requery();
+ image.onRemove();
+ }
+ dump("after delete");
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ protected void requery() {
+ mCache.clear();
+ mCursor.requery();
+ mCursorDeactivated = false;
+ }
+
+ protected void saveMiniThumbToFile(Bitmap source, long id, long magic) {
+ RandomAccessFile r = miniThumbDataFile();
+ if (r == null)
+ return;
+
+ long pos = id * sBytesPerMiniThumb;
+ long t0 = System.currentTimeMillis();
+ synchronized (r) {
+ try {
+ long t1 = System.currentTimeMillis();
+ byte [] data = miniThumbData(source);
+ long t2 = System.currentTimeMillis();
+ if (data != null) {
+ if (data.length > sBytesPerMiniThumb) {
+ if (VERBOSE) Log.v(TAG, "!!!!!!!!!!!!!!!!!!!!!!!!!!! " + data.length + " > " + sBytesPerMiniThumb);
+ return;
+ }
+ r.seek(pos);
+ r.writeByte(0); // we have no data in this slot
+
+ // if magic is 0 then leave it alone
+ if (magic == 0)
+ r.skipBytes(8);
+ else
+ r.writeLong(magic);
+ r.writeInt(data.length);
+ r.write(data);
+ // f.flush();
+ r.seek(pos);
+ r.writeByte(1); // we have data in this slot
+ long t3 = System.currentTimeMillis();
+
+ if (VERBOSE) Log.v(TAG, "saveMiniThumbToFile took " + (t3-t0) + "; " + (t1-t0) + " " + (t2-t1) + " " + (t3-t2));
+ }
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't save mini thumbnail data for " + id + "; " + ex.toString());
+ }
+ }
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+ }
+
+ public class CanceledException extends Exception {
+
+ }
+ public enum DataLocation { NONE, INTERNAL, EXTERNAL, ALL }
+
+ public interface IAddImage_cancelable extends ICancelable {
+ public void get();
+ }
+
+ /*
+ * The model for canceling an in-progress image save is this. For any
+ * given part of the task of saving return an ICancelable. The "result"
+ * from an ICancelable can be retrieved using the get* method. If the
+ * operation was canceled then null is returned. The act of canceling
+ * is to call "cancel" -- from another thread.
+ *
+ * In general an object which implements ICancelable will need to
+ * check, periodically, whether they are canceled or not. This works
+ * well for some things and less well for others.
+ *
+ * Right now the actual jpeg encode does not check cancelation but
+ * the part of encoding which writes the data to disk does. Note,
+ * though, that there is what appears to be a bug in the jpeg encoder
+ * in that if the stream that's being written is closed it crashes
+ * rather than returning an error. TODO fix that.
+ *
+ * When an object detects that it is canceling it must, before exiting,
+ * call acknowledgeCancel. This is necessary because the caller of
+ * cancel() will block until acknowledgeCancel is called.
+ */
+ public interface ICancelable {
+ /*
+ * call cancel() when the unit of work in progress needs to be
+ * canceled. This should return true if it was possible to
+ * cancel and false otherwise. If this returns false the caller
+ * may still be able to cleanup and simulate cancelation.
+ */
+ public boolean cancel();
+ }
+
+ public interface IGetBitmap_cancelable extends ICancelable {
+ // returns the bitmap or null if there was an error or we were canceled
+ public Bitmap get();
+ };
+ public interface IGetBoolean_cancelable extends ICancelable {
+ public boolean get();
+ }
+ public interface IImage {
+
+ public abstract void commitChanges();
+
+ /**
+ * Get the bitmap for the full size image.
+ * @return the bitmap for the full size image.
+ */
+ public abstract Bitmap fullSizeBitmap(int targetWidthOrHeight);
+
+ /**
+ *
+ * @return an object which can be canceled while the bitmap is loading
+ */
+ public abstract IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight);
+
+ /**
+ * Gets the input stream associated with a given full size image.
+ * This is used, for example, if one wants to email or upload
+ * the image.
+ * @return the InputStream associated with the image.
+ */
+ public abstract InputStream fullSizeImageData();
+ public abstract long fullSizeImageId();
+ public abstract Uri fullSizeImageUri();
+ public abstract IImageList getContainer();
+ public abstract long getDateTaken();
+
+ /**
+ * Gets the description of the image.
+ * @return the description of the image.
+ */
+ public abstract String getDescription();
+ public abstract String getMimeType();
+ public abstract int getHeight();
+
+ /**
+ * Gets the flag telling whether this video/photo is private or public.
+ * @return the description of the image.
+ */
+ public abstract boolean getIsPrivate();
+
+ public abstract double getLatitude();
+
+ public abstract double getLongitude();
+
+ /**
+ * Gets the name of the image.
+ * @return the name of the image.
+ */
+ public abstract String getTitle();
+
+ public abstract String getDisplayName();
+
+ public abstract String getPicasaId();
+
+ public abstract int getRow();
+
+ public abstract int getWidth();
+
+ public abstract boolean hasLatLong();
+
+ public abstract long imageId();
+
+ public abstract boolean isReadonly();
+
+ public abstract boolean isDrm();
+
+ public abstract Bitmap miniThumbBitmap();
+
+ public abstract void onRemove();
+
+ public abstract boolean rotateImageBy(int degrees);
+
+ /**
+ * Sets the description of the image.
+ */
+ public abstract void setDescription(String description);
+
+ /**
+ * Sets whether the video/photo is private or public.
+ */
+ public abstract void setIsPrivate(boolean isPrivate);
+
+ /**
+ * Sets the name of the image.
+ */
+ public abstract void setName(String name);
+
+ public abstract void setPicasaId(String id);
+
+ /**
+ * Get the bitmap for the medium thumbnail.
+ * @return the bitmap for the medium thumbnail.
+ */
+ public abstract Bitmap thumbBitmap();
+
+ public abstract Uri thumbUri();
+
+ public abstract String getDataPath();
+ }
+ public interface IImageList {
+ public HashMap<String, String> getBucketIds();
+
+ public interface OnChange {
+ public void onChange(IImageList list);
+ }
+
+ public interface ThumbCheckCallback {
+ public boolean checking(int current, int count);
+ }
+
+ public abstract void checkThumbnails(ThumbCheckCallback cb);
+
+ public abstract void commitChanges();
+
+ public abstract void deactivate();
+
+ /**
+ * Returns the count of image objects.
+ *
+ * @return the number of images
+ */
+ public abstract int getCount();
+
+ /**
+ * Returns the image at the ith position.
+ *
+ * @param i the position
+ * @return the image at the ith position
+ */
+ public abstract IImage getImageAt(int i);
+
+ /**
+ * Returns the image with a particular Uri.
+ *
+ * @param uri
+ * @return the image with a particular Uri.
+ */
+ public abstract IImage getImageForUri(Uri uri);;
+
+ public abstract boolean removeImage(IImage image);
+ /**
+ * Removes the image at the ith position.
+ * @param i the position
+ */
+ public abstract void removeImageAt(int i);
+
+ public abstract void removeOnChangeListener(OnChange changeCallback);
+ public abstract void setOnChangeListener(OnChange changeCallback, Handler h);
+ }
+
+ class VideoObject extends Image {
+ public VideoObject() {
+ super(0, 0, null, null, 0, 0);
+ }
+
+ public String getTags() {
+ return null;
+ }
+
+ public String setTags(String tags) {
+ return null;
+ }
+ }
+
+ class Image extends BaseImage implements IImage {
+ int mRotation;
+
+ protected Image(long id, long miniThumbId, ContentResolver cr, BaseImageList container, int cursorRow, int rotation) {
+ super(id, miniThumbId, cr, container, cursorRow);
+ mRotation = rotation;
+ }
+
+ public String getDataPath() {
+ String path = null;
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexData();
+ if (column >= 0)
+ path = c.getString(column);
+ }
+ }
+ return path;
+ }
+
+ protected int getDegreesRotated() {
+ return mRotation;
+ }
+
+ protected void setDegreesRotated(int degrees) {
+ Cursor c = getCursor();
+ mRotation = degrees;
+ synchronized (c) {
+ if (c.moveToPosition(getRow())) {
+ int column = ((ImageList)getContainer()).indexOrientation();
+ if (column >= 0) {
+ c.updateInt(column, degrees);
+ getContainer().commitChanges();
+ }
+ }
+ }
+ }
+
+ protected Bitmap.CompressFormat compressionType() {
+ String mimeType = getMimeType();
+ if (mimeType == null)
+ return Bitmap.CompressFormat.JPEG;
+
+ if (mimeType.equals("image/png"))
+ return Bitmap.CompressFormat.PNG;
+ else if (mimeType.equals("image/png"))
+ return Bitmap.CompressFormat.PNG;
+
+ return Bitmap.CompressFormat.JPEG;
+ }
+
+ /**
+ * Does not replace the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void addExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.put(tag, value);
+ } else {
+ if (VERBOSE) Log.v(TAG, "addExifTag where the key already was there: " + tag + " = " + value);
+ }
+ }
+
+ /**
+ * Return the value of the Exif tag as an int. Returns 0 on any type of error.
+ * @param tag
+ * @return
+ */
+ public int getExifTagInt(String tag) {
+ if (mExifData != null) {
+ String tagValue = mExifData.get(tag);
+ if (tagValue != null) {
+ return Integer.parseInt(tagValue);
+ }
+ }
+ return 0;
+ }
+
+ public boolean isReadonly() {
+ String mimeType = getMimeType();
+ return !"image/jpeg".equals(mimeType) && !"image/png".equals(mimeType);
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ /**
+ * Remove tag if already there. Otherwise, does nothing.
+ * @param tag
+ */
+ public void removeExifTag(String tag) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ mExifData.remove(tag);
+ }
+
+ /**
+ * Replaces the tag if already there. Otherwise, adds to the exif tags.
+ * @param tag
+ * @param value
+ */
+ public void replaceExifTag(String tag, String value) {
+ if (mExifData == null) {
+ mExifData = new HashMap<String, String>();
+ }
+ if (!mExifData.containsKey(tag)) {
+ mExifData.remove(tag);
+ }
+ mExifData.put(tag, value);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.IImage#saveModifiedImage(android.graphics.Bitmap)
+ */
+ public IGetBoolean_cancelable saveImageContents(
+ final Bitmap image,
+ final byte [] jpegData,
+ final int orientation,
+ final boolean newFile,
+ final Cursor cursor) {
+ final class SaveImageContentsCancelable extends BaseCancelable implements IGetBoolean_cancelable {
+ IGetBoolean_cancelable mCurrentCancelable = null;
+
+ SaveImageContentsCancelable() {
+ }
+
+ public boolean doCancelWork() {
+ synchronized (this) {
+ if (mCurrentCancelable != null)
+ mCurrentCancelable.cancel();
+ }
+ return true;
+ }
+
+ public boolean get() {
+ try {
+ Bitmap thumbnail = null;
+
+ long t1 = System.currentTimeMillis();
+ Uri uri = mContainer.contentUri(mId);
+ synchronized (this) {
+ checkCanceled();
+ mCurrentCancelable = compressImageToFile(image, jpegData, uri);
+ }
+
+ long t2 = System.currentTimeMillis();
+ if (!mCurrentCancelable.get())
+ return false;
+
+ synchronized (this) {
+ String filePath;
+ synchronized (cursor) {
+ cursor.moveToPosition(0);
+ filePath = cursor.getString(2);
+ }
+ // TODO: If thumbData is present and usable, we should call the version
+ // of storeThumbnail which takes a byte array, rather than re-encoding
+ // a new JPEG of the same dimensions.
+ byte [] thumbData = null;
+ synchronized (ImageManager.instance()) {
+ thumbData = (new ExifInterface(filePath)).getThumbnail();
+ }
+ if (VERBOSE) Log.v(TAG, "for file " + filePath + " thumbData is " + thumbData + "; length " + (thumbData!=null ? thumbData.length : -1));
+ if (thumbData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(thumbData, 0, thumbData.length);
+ if (VERBOSE) Log.v(TAG, "embedded thumbnail bitmap " + thumbnail.getWidth() + "/" + thumbnail.getHeight());
+ }
+ if (thumbnail == null && image != null) {
+ thumbnail = image;
+ }
+ if (thumbnail == null && jpegData != null) {
+ thumbnail = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
+ }
+ }
+
+ long t3 = System.currentTimeMillis();
+ mContainer.storeThumbnail(thumbnail, Image.this.fullSizeImageId());
+ long t4 = System.currentTimeMillis();
+ checkCanceled();
+ if (VERBOSE) Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>> rotating by " + orientation);
+ saveMiniThumb(rotate(thumbnail, orientation));
+ long t5 = System.currentTimeMillis();
+ checkCanceled();
+
+ if (VERBOSE) Log.v(TAG, String.format("Timing data %d %d %d %d", t2-t1, t3-t2, t4-t3, t5-t4));
+ return true;
+ } catch (CanceledException ex) {
+ if (VERBOSE) Log.v(TAG, "got canceled... need to cleanup");
+ return false;
+ } finally {
+ /*
+ Cursor c = getCursor();
+ synchronized (c) {
+ if (c.moveTo(getRow())) {
+ mContainer.requery();
+ }
+ }
+ */
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new SaveImageContentsCancelable();
+ }
+
+ private void setExifRotation(int degrees) {
+ try {
+ Cursor c = getCursor();
+ String filePath;
+ synchronized (c) {
+ filePath = c.getString(mContainer.indexData());
+ }
+ synchronized (ImageManager.instance()) {
+ ExifInterface exif = new ExifInterface(filePath);
+ if (mExifData == null) {
+ mExifData = exif.getAttributes();
+ }
+ if (degrees < 0)
+ degrees += 360;
+
+ int orientation = ExifInterface.ORIENTATION_NORMAL;
+ switch (degrees) {
+ case 0:
+ orientation = ExifInterface.ORIENTATION_NORMAL;
+ break;
+ case 90:
+ orientation = ExifInterface.ORIENTATION_ROTATE_90;
+ break;
+ case 180:
+ orientation = ExifInterface.ORIENTATION_ROTATE_180;
+ break;
+ case 270:
+ orientation = ExifInterface.ORIENTATION_ROTATE_270;
+ break;
+ }
+
+ replaceExifTag(ExifInterface.TAG_ORIENTATION, Integer.toString(orientation));
+ replaceExifTag("UserComment", "saveRotatedImage comment orientation: " + orientation);
+ exif.saveAttributes(mExifData);
+ exif.commitChanges();
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "unable to save exif data with new orientation " + fullSizeImageUri());
+ }
+ }
+
+ /**
+ * Save the rotated image by updating the Exif "Orientation" tag.
+ * @param degrees
+ * @return
+ */
+ public boolean rotateImageBy(int degrees) {
+ int newDegrees = getDegreesRotated() + degrees;
+ setExifRotation(newDegrees);
+ setDegreesRotated(newDegrees);
+
+ // setting this to zero will force the call to checkCursor to generate fresh thumbs
+ mMiniThumbMagic = 0;
+ Cursor c = mContainer.getCursor();
+ synchronized (c) {
+ mContainer.checkThumbnail(this, mContainer.getCursor(), this.getRow());
+ }
+
+ return true;
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap bitmap = null;
+ Cursor c = null;
+ if (mContainer.mThumbUri != null) {
+ try {
+ c = mContentResolver.query(
+ mContainer.mThumbUri,
+ THUMB_PROJECTION,
+ Thumbnails.IMAGE_ID + "=?",
+ new String[] { String.valueOf(fullSizeImageId()) },
+ null);
+ if (c != null && c.moveToFirst()) {
+ Uri thumbUri = ContentUris.withAppendedId(mContainer.mThumbUri, c.getLong(((ImageList)mContainer).INDEX_THUMB_ID));
+ ParcelFileDescriptor pfdInput;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ pfdInput = mContentResolver.openFileDescriptor(thumbUri, "r");
+ bitmap = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ pfdInput.close();
+ } catch (FileNotFoundException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (IOException ex) {
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ } catch (NullPointerException ex) {
+ // we seem to get this if the file doesn't exist anymore
+ Log.e(TAG, "couldn't open thumbnail " + thumbUri + "; " + ex);
+ }
+ }
+ } catch (Exception ex) {
+ // sdcard removed?
+ return null;
+ } finally {
+ if (c != null)
+ c.close();
+ }
+ }
+
+ if (bitmap == null) {
+ bitmap = fullSizeBitmap(THUMBNAIL_TARGET_SIZE, false);
+ if (VERBOSE) {
+ Log.v(TAG, "no thumbnail found... storing new one for " + fullSizeImageId());
+ }
+ bitmap = mContainer.storeThumbnail(bitmap, fullSizeImageId());
+ }
+
+ if (bitmap != null) {
+ int degrees = getDegreesRotated();
+ if (degrees != 0) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(),
+ m, true);
+ }
+ }
+
+ long elapsed = System.currentTimeMillis();
+ return bitmap;
+ }
+
+ }
+
+ final static private String sWhereClause = "(" + Images.Media.MIME_TYPE + "=? or " + Images.Media.MIME_TYPE + "=?" + ")";
+ final static private String[] sAcceptableImageTypes = new String[] { "image/jpeg", "image/png" };
+
+ private static final String[] IMAGE_PROJECTION = new String[] {
+ "_id",
+ "_data",
+ ImageColumns.DATE_TAKEN,
+ ImageColumns.MINI_THUMB_MAGIC,
+ ImageColumns.ORIENTATION,
+ ImageColumns.MIME_TYPE
+ };
+
+ /**
+ * Represents an ordered collection of Image objects.
+ * Provides an api to add and remove an image.
+ */
+ class ImageList extends BaseImageList implements IImageList {
+ final int INDEX_ID = indexOf(IMAGE_PROJECTION, "_id");
+ final int INDEX_DATA = indexOf(IMAGE_PROJECTION, "_data");
+ final int INDEX_MIME_TYPE = indexOf(IMAGE_PROJECTION, MediaColumns.MIME_TYPE);
+ final int INDEX_DATE_TAKEN = indexOf(IMAGE_PROJECTION, ImageColumns.DATE_TAKEN);
+ final int INDEX_MINI_THUMB_MAGIC = indexOf(IMAGE_PROJECTION, ImageColumns.MINI_THUMB_MAGIC);
+ final int INDEX_ORIENTATION = indexOf(IMAGE_PROJECTION, ImageColumns.ORIENTATION);
+
+ final int INDEX_THUMB_ID = indexOf(THUMB_PROJECTION, BaseColumns._ID);
+ final int INDEX_THUMB_IMAGE_ID = indexOf(THUMB_PROJECTION, Images.Thumbnails.IMAGE_ID);
+ final int INDEX_THUMB_WIDTH = indexOf(THUMB_PROJECTION, Images.Thumbnails.WIDTH);
+ final int INDEX_THUMB_HEIGHT = indexOf(THUMB_PROJECTION, Images.Thumbnails.HEIGHT);
+
+ boolean mIsRegistered = false;
+ ContentObserver mContentObserver;
+ DataSetObserver mDataSetObserver;
+
+ public HashMap<String, String> getBucketIds() {
+ Cursor c = Images.Media.query(
+ mContentResolver,
+ mBaseUri.buildUpon().appendQueryParameter("distinct", "true").build(),
+ new String[] {
+ ImageColumns.BUCKET_DISPLAY_NAME,
+ ImageColumns.BUCKET_ID
+ },
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+
+ HashMap<String, String> hash = new HashMap<String, String>();
+ if (c != null && c.moveToFirst()) {
+ do {
+ hash.put(c.getString(1), c.getString(0));
+ } while (c.moveToNext());
+ }
+ return hash;
+ }
+ /**
+ * ImageList constructor.
+ * @param cr ContentResolver
+ */
+ public ImageList(Context ctx, ContentResolver cr, Uri imageUri, Uri thumbUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, sort, bucketId);
+ mBaseUri = imageUri;
+ mThumbUri = thumbUri;
+ mSort = sort;
+ mBucketId = bucketId;
+
+ mContentResolver = cr;
+
+ mCursor = createCursor();
+ if (mCursor == null) {
+ Log.e(TAG, "unable to create image cursor for " + mBaseUri);
+ throw new UnsupportedOperationException();
+ }
+
+ if (VERBOSE) {
+ Log.v(TAG, "for " + mBaseUri.toString() + " got cursor " + mCursor + " with length " + (mCursor != null ? mCursor.getCount() : "-1"));
+ }
+
+ final Runnable updateRunnable = new Runnable() {
+ public void run() {
+ // handling these external updates is causing ANR problems that are unresolved.
+ // For now ignore them since there shouldn't be anyone modifying the database on the fly.
+ if (true)
+ return;
+
+ synchronized (mCursor) {
+ requery();
+ }
+ if (mListener != null)
+ mListener.onChange(ImageList.this);
+ }
+ };
+
+ mContentObserver = new ContentObserver(null) {
+ @Override
+ public boolean deliverSelfNotifications() {
+ return false;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (VERBOSE) Log.v(TAG, "MyContentObserver.onChange; selfChange == " + selfChange);
+ updateRunnable.run();
+ }
+ };
+
+ mDataSetObserver = new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onChanged");
+// updateRunnable.run();
+ }
+
+ @Override
+ public void onInvalidated() {
+ if (VERBOSE) Log.v(TAG, "MyDataSetObserver.onInvalidated: " + mCursorDeactivated);
+ }
+ };
+
+ registerObservers();
+ }
+
+ private void registerObservers() {
+ if (mIsRegistered)
+ return;
+
+ mCursor.registerContentObserver(mContentObserver);
+ mCursor.registerDataSetObserver(mDataSetObserver);
+ mIsRegistered = true;
+ }
+
+ private void unregisterObservers() {
+ if (!mIsRegistered)
+ return;
+
+ mCursor.unregisterContentObserver(mContentObserver);
+ mCursor.unregisterDataSetObserver(mDataSetObserver);
+ mIsRegistered = false;
+ }
+
+ public void deactivate() {
+ super.deactivate();
+ unregisterObservers();
+ }
+
+ protected void activateCursor() {
+ super.activateCursor();
+ registerObservers();
+ }
+
+ protected String whereClause() {
+ if (mBucketId != null) {
+ return sWhereClause + " and " + Images.Media.BUCKET_ID + " = " + mBucketId;
+ } else {
+ return sWhereClause;
+ }
+ }
+
+ protected String[] whereClauseArgs() {
+ return sAcceptableImageTypes;
+ }
+
+ protected Cursor createCursor() {
+ Cursor c =
+ Images.Media.query(
+ mContentResolver,
+ mBaseUri,
+ IMAGE_PROJECTION,
+ whereClause(),
+ whereClauseArgs(),
+ sortOrder());
+ if (VERBOSE)
+ Log.v(TAG, "createCursor got cursor with count " + (c == null ? -1 : c.getCount()));
+ return c;
+ }
+
+ protected int indexOrientation() { return INDEX_ORIENTATION; }
+ protected int indexDateTaken() { return INDEX_DATE_TAKEN; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return INDEX_MIME_TYPE; }
+ protected int indexData() { return INDEX_DATA; }
+ protected int indexId() { return INDEX_ID; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return INDEX_MINI_THUMB_MAGIC; }
+
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return INDEX_THUMB_ID; }
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index, int rotation) {
+ return new Image(id, miniThumbId, mContentResolver, this, index, rotation);
+ }
+
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfd, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (pfd == null)
+ pfd = makeInputStream(uri);
+
+ if (pfd == null)
+ return null;
+
+ if (options == null)
+ options = new BitmapFactory.Options();
+
+ java.io.FileDescriptor fd = pfd.getFileDescriptor();
+ options.inSampleSize = 1;
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ long t1 = System.currentTimeMillis();
+ BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {
+ return null;
+ }
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ long t1 = System.currentTimeMillis();
+ b = BitmapFactory.decodeFileDescriptor(fd, null, options);
+ long t2 = System.currentTimeMillis();
+ if (VERBOSE) {
+ Log.v(TAG, "A: got bitmap " + b + " with sampleSize " + options.inSampleSize + " took " + (t2-t1));
+ }
+ pfd.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ return b;
+ }
+
+ private ParcelFileDescriptor makeInputStream(Uri uri) {
+ try {
+ return mContentResolver.openFileDescriptor(uri, "r");
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ private String sortOrder() {
+ // add id to the end so that we don't ever get random sorting
+ // which could happen, I suppose, if the first two values were
+ // duplicated
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ Images.Media.DATE_TAKEN + ascending + "," +
+ Images.Media._ID + ascending;
+ }
+
+ }
+
+ /**
+ * Represents an ordered collection of Image objects from the DRM provider.
+ */
+ class DrmImageList extends ImageList implements IImageList {
+ private final String[] DRM_IMAGE_PROJECTION = new String[] {
+ DrmStore.Audio._ID,
+ DrmStore.Audio.DATA,
+ DrmStore.Audio.MIME_TYPE,
+ };
+
+ final int INDEX_ID = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio._ID);
+ final int INDEX_MIME_TYPE = indexOf(DRM_IMAGE_PROJECTION, DrmStore.Audio.MIME_TYPE);
+
+ public DrmImageList(Context ctx, ContentResolver cr, Uri imageUri, int sort, String bucketId) {
+ super(ctx, cr, imageUri, null, sort, bucketId);
+ }
+
+ protected Cursor createCursor() {
+ return mContentResolver.query(mBaseUri, DRM_IMAGE_PROJECTION, null, null, sortOrder());
+ }
+
+ @Override
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ // do nothing
+ }
+
+ @Override
+ public long checkThumbnail(BaseImage existingImage, Cursor c, int i) {
+ return 0;
+ }
+
+ class DrmImage extends Image {
+ protected DrmImage(long id, ContentResolver cr, BaseImageList container, int cursorRow) {
+ super(id, 0, cr, container, cursorRow, 0);
+ }
+
+ public boolean isDrm() {
+ return true;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return fullSizeBitmap(MINI_THUMB_TARGET_SIZE);
+ }
+
+ public Bitmap thumbBitmap() {
+ return fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ }
+ }
+
+ protected IImage make(long id, long miniThumbId, ContentResolver cr, IImageList list, long timestamp, int index) {
+ return new DrmImage(id, mContentResolver, this, index);
+ }
+
+ protected int indexOrientation() { return -1; }
+ protected int indexDateTaken() { return -1; }
+ protected int indexDescription() { return -1; }
+ protected int indexMimeType() { return -1; }
+ protected int indexId() { return -1; }
+ protected int indexLatitude() { return -1; }
+ protected int indexLongitude() { return -1; }
+ protected int indexMiniThumbId() { return -1; }
+ protected int indexPicasaWeb() { return -1; }
+ protected int indexPrivate() { return -1; }
+ protected int indexTitle() { return -1; }
+ protected int indexDisplayName() { return -1; }
+ protected int indexThumbId() { return -1; }
+
+ // TODO review this probably should be based on DATE_TAKEN same as images
+ private String sortOrder() {
+ String ascending = (mSort == SORT_ASCENDING ? " ASC" : " DESC");
+ return
+ DrmStore.Images.TITLE + ascending + "," +
+ DrmStore.Images._ID;
+ }
+ }
+
+ class ImageListUber implements IImageList {
+ private IImageList [] mSubList;
+ private int mSort;
+ private IImageList.OnChange mListener = null;
+ Handler mHandler;
+
+ // This is an array of Longs wherein each Long consists of
+ // two components. The first component indicates the number of
+ // consecutive entries that belong to a given sublist.
+ // The second component indicates which sublist we're referring
+ // to (an int which is used to index into mSubList).
+ ArrayList<Long> mSkipList = null;
+
+ int [] mSkipCounts = null;
+
+ public HashMap<String, String> getBucketIds() {
+ HashMap<String, String> hashMap = new HashMap<String, String>();
+ for (IImageList list: mSubList) {
+ hashMap.putAll(list.getBucketIds());
+ }
+ return hashMap;
+ }
+
+ public ImageListUber(IImageList [] sublist, int sort) {
+ mSubList = sublist.clone();
+ mSort = sort;
+
+ if (mListener != null) {
+ for (IImageList list: sublist) {
+ list.setOnChangeListener(new OnChange() {
+ public void onChange(IImageList list) {
+ if (mListener != null) {
+ mListener.onChange(ImageListUber.this);
+ }
+ }
+ }, mHandler);
+ }
+ }
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ // TODO this isn't quite right because we need to get the
+ // total from each sub item and provide that in the callback
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++)
+ sublist[i].checkThumbnails(cb);
+ }
+
+ public void commitChanges() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ for (int i = 0; i < length; i++)
+ sublist[i].commitChanges();
+ }
+
+ public void deactivate() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ int pos = -1;
+ while (++pos < length) {
+ IImageList sub = sublist[pos];
+ sub.deactivate();
+ }
+ }
+
+ public int getCount() {
+ final IImageList sublist[] = mSubList;
+ final int length = sublist.length;
+ int count = 0;
+ for (int i = 0; i < length; i++)
+ count += sublist[i].getCount();
+ return count;
+ }
+
+ // mSkipCounts is used to tally the counts as we traverse
+ // the mSkipList. It's a member variable only so that
+ // we don't have to allocate each time through. Otherwise
+ // it could just as easily be a local.
+
+ public synchronized IImage getImageAt(int index) {
+ if (index < 0 || index > getCount())
+ throw new IndexOutOfBoundsException("index " + index + " out of range max is " + getCount());
+
+ // first make sure our allocations are in order
+ if (mSkipCounts == null || mSubList.length > mSkipCounts.length)
+ mSkipCounts = new int[mSubList.length];
+
+ if (mSkipList == null)
+ mSkipList = new ArrayList<Long>();
+
+ // zero out the mSkipCounts since that's only used for the
+ // duration of the function call
+ for (int i = 0; i < mSubList.length; i++)
+ mSkipCounts[i] = 0;
+
+ // a counter of how many images we've skipped in
+ // trying to get to index. alternatively we could
+ // have decremented index but, alas, I liked this
+ // way more.
+ int skipCount = 0;
+
+ // scan the existing mSkipList to see if we've computed
+ // enough to just return the answer
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ int subindex = mSkipCounts[which] + (index - skipCount);
+ IImage img = mSubList[which].getImageAt(subindex);
+ return img;
+ }
+
+ skipCount += offset;
+ mSkipCounts[which] += offset;
+ }
+
+ // if we get here we haven't computed the answer for
+ // "index" yet so keep computing. This means running
+ // through the list of images and either modifying the
+ // last entry or creating a new one.
+ long count = 0;
+ while (true) {
+ long maxTimestamp = mSort == SORT_ASCENDING ? Long.MAX_VALUE : Long.MIN_VALUE;
+ int which = -1;
+ for (int i = 0; i < mSubList.length; i++) {
+ int pos = mSkipCounts[i];
+ IImageList list = mSubList[i];
+ if (pos < list.getCount()) {
+ IImage image = list.getImageAt(pos);
+ // this should never be null but sometimes the database is
+ // causing problems and it is null
+ if (image != null) {
+ long timestamp = image.getDateTaken();
+ if (mSort == SORT_ASCENDING ? (timestamp < maxTimestamp) : (timestamp > maxTimestamp)) {
+ maxTimestamp = timestamp;
+ which = i;
+ }
+ }
+ }
+ }
+
+ if (which == -1) {
+ if (VERBOSE) Log.v(TAG, "which is -1, returning null");
+ return null;
+ }
+
+ boolean done = false;
+ count = 1;
+ if (mSkipList.size() > 0) {
+ int pos = mSkipList.size() - 1;
+ long oldEntry = mSkipList.get(pos);
+ if ((oldEntry >> 32) == which) {
+ long newEntry = oldEntry + 1;
+ mSkipList.set(pos, newEntry);
+ done = true;
+ }
+ }
+ if (!done) {
+ long newEntry = ((long)which << 32) | count;
+ if (VERBOSE) {
+ Log.v(TAG, "new entry is " + Long.toHexString(newEntry));
+ }
+ mSkipList.add(newEntry);
+ }
+
+ if (skipCount++ == index) {
+ return mSubList[which].getImageAt(mSkipCounts[which]);
+ }
+ mSkipCounts[which] += 1;
+ }
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ // TODO perhaps we can preflight the base of the uri
+ // against each sublist first
+ for (int i = 0; i < mSubList.length; i++) {
+ IImage img = mSubList[i].getImageForUri(uri);
+ if (img != null)
+ return img;
+ }
+ return null;
+ }
+
+ /**
+ * Modify the skip list when an image is deleted by finding
+ * the relevant entry in mSkipList and decrementing the
+ * counter. This is simple because deletion can never
+ * cause change the order of images.
+ */
+ public void modifySkipCountForDeletedImage(int index) {
+ int skipCount = 0;
+
+ for (int i = 0; i < mSkipList.size(); i++) {
+ long v = mSkipList.get(i);
+
+ int offset = (int) (v & 0xFFFF);
+ int which = (int) (v >> 32);
+
+ if (skipCount + offset > index) {
+ mSkipList.set(i, v-1);
+ break;
+ }
+
+ skipCount += offset;
+ }
+ }
+
+ public boolean removeImage(IImage image) {
+ int pos = -1;
+ while (++pos < mSubList.length) {
+ IImageList sub = mSubList[pos];
+ if (sub.removeImage(image)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void removeImageAt(int index) {
+ IImage img = getImageAt(index);
+ if (img != null) {
+ IImageList list = img.getContainer();
+ if (list != null) {
+ list.removeImage(img);
+ modifySkipCountForDeletedImage(index);
+ }
+ }
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ if (changeCallback == mListener)
+ mListener = null;
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ mListener = changeCallback;
+ mHandler = h;
+ }
+
+ }
+
+ public static abstract class SimpleBaseImage implements IImage {
+ public void commitChanges() {
+ throw new UnsupportedOperationException();
+ }
+
+ public InputStream fullSizeImageData() {
+ throw new UnsupportedOperationException();
+ }
+
+ public long fullSizeImageId() {
+ return 0;
+ }
+
+ public Uri fullSizeImageUri() {
+ throw new UnsupportedOperationException();
+ }
+
+ public IImageList getContainer() {
+ return null;
+ }
+
+ public long getDateTaken() {
+ return 0;
+ }
+
+ public String getMimeType() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDescription() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean getIsPrivate() {
+ throw new UnsupportedOperationException();
+ }
+
+ public double getLatitude() {
+ return 0D;
+ }
+
+ public double getLongitude() {
+ return 0D;
+ }
+
+ public String getTitle() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getDisplayName() {
+ throw new UnsupportedOperationException();
+ }
+
+ public String getPicasaId() {
+ return null;
+ }
+
+ public int getRow() {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getHeight() {
+ return 0;
+ }
+
+ public int getWidth() {
+ return 0;
+ }
+
+ public boolean hasLatLong() {
+ return false;
+ }
+
+ public boolean isReadonly() {
+ return true;
+ }
+
+ public boolean isDrm() {
+ return false;
+ }
+
+ public void onRemove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean rotateImageBy(int degrees) {
+ return false;
+ }
+
+ public void setDescription(String description) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setIsPrivate(boolean isPrivate) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setName(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void setPicasaId(long id) {
+ }
+
+ public void setPicasaId(String id) {
+ }
+
+ public Uri thumbUri() {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ class SingleImageList extends BaseImageList implements IImageList {
+ private IImage mSingleImage;
+ private ContentResolver mContentResolver;
+ private Uri mUri;
+
+ class UriImage extends SimpleBaseImage {
+
+ UriImage() {
+ }
+
+ public String getDataPath() {
+ return mUri.getPath();
+ }
+
+ InputStream getInputStream() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return new java.io.FileInputStream(mUri.getPath());
+ } else {
+ return mContentResolver.openInputStream(mUri);
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ ParcelFileDescriptor getPFD() {
+ try {
+ if (mUri.getScheme().equals("file")) {
+ String path = mUri.getPath();
+ if (VERBOSE)
+ Log.v(TAG, "path is " + path);
+ return ParcelFileDescriptor.open(new File(path), ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ return mContentResolver.openFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException ex) {
+ return null;
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.camera.ImageManager.IImage#fullSizeBitmap(int)
+ */
+ public Bitmap fullSizeBitmap(int targetWidthHeight) {
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ if (targetWidthHeight != -1)
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+
+ options.inJustDecodeBounds = false;
+ options.inDither = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ Bitmap b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "B: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ pfdInput.close();
+ return b;
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception decoding bitmap " + ex.toString());
+ return null;
+ }
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(final int targetWidthOrHeight) {
+ final class LoadBitmapCancelable extends BaseCancelable implements IGetBitmap_cancelable {
+ ParcelFileDescriptor pfdInput;
+ BitmapFactory.Options mOptions = new BitmapFactory.Options();
+ long mCancelInitiationTime;
+
+ public LoadBitmapCancelable(ParcelFileDescriptor pfd) {
+ pfdInput = pfd;
+ }
+
+ public boolean doCancelWork() {
+ if (VERBOSE)
+ Log.v(TAG, "requesting bitmap load cancel");
+ mCancelInitiationTime = System.currentTimeMillis();
+ mOptions.requestCancelDecode();
+ return true;
+ }
+
+ public Bitmap get() {
+ try {
+ Bitmap b = makeBitmap(targetWidthOrHeight, fullSizeImageUri(), pfdInput, mOptions);
+ if (b == null && mCancelInitiationTime != 0) {
+ if (VERBOSE)
+ Log.v(TAG, "cancel returned null bitmap -- took " + (System.currentTimeMillis()-mCancelInitiationTime));
+ }
+ if (VERBOSE) Log.v(TAG, "b is " + b);
+ return b;
+ } catch (Exception ex) {
+ return null;
+ } finally {
+ acknowledgeCancel();
+ }
+ }
+ }
+
+ try {
+ ParcelFileDescriptor pfdInput = getPFD();
+ if (pfdInput == null)
+ return null;
+ if (VERBOSE) Log.v(TAG, "inputStream is " + pfdInput);
+ return new LoadBitmapCancelable(pfdInput);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ public Uri fullSizeImageUri() {
+ return mUri;
+ }
+
+ @Override
+ public InputStream fullSizeImageData() {
+ return getInputStream();
+ }
+
+ public long imageId() {
+ return 0;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return thumbBitmap();
+ }
+
+ @Override
+ public String getTitle() {
+ return mUri.toString();
+ }
+
+ @Override
+ public String getDisplayName() {
+ return getTitle();
+ }
+
+ @Override
+ public String getDescription() {
+ return "";
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap b = fullSizeBitmap(THUMBNAIL_TARGET_SIZE);
+ if (b != null) {
+ Matrix m = new Matrix();
+ float scale = Math.min(1F, THUMBNAIL_TARGET_SIZE / (float) b.getWidth());
+ m.setScale(scale, scale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ return scaledBitmap;
+ } else {
+ return null;
+ }
+ }
+
+ private BitmapFactory.Options snifBitmapOptions() {
+ ParcelFileDescriptor input = getPFD();
+ if (input == null)
+ return null;
+ try {
+ Uri uri = fullSizeImageUri();
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(input.getFileDescriptor(), null, options);
+ return options;
+ } finally {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ex) {
+ }
+ }
+ }
+
+ @Override
+ public String getMimeType() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outMimeType : "";
+ }
+
+ @Override
+ public int getHeight() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outHeight : 0;
+ }
+
+ @Override
+ public int getWidth() {
+ BitmapFactory.Options options = snifBitmapOptions();
+ return (options!=null) ? options.outWidth : 0;
+ }
+ }
+
+ public SingleImageList(ContentResolver cr, Uri uri) {
+ super(null, cr, uri, ImageManager.SORT_ASCENDING, null);
+ mContentResolver = cr;
+ mUri = uri;
+ mSingleImage = new UriImage();
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void deactivate() {
+ // nothing to do here
+ }
+
+ public int getCount() {
+ return 1;
+ }
+
+ public IImage getImageAt(int i) {
+ if (i == 0)
+ return mSingleImage;
+
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ if (uri.equals(mUri))
+ return mSingleImage;
+ else
+ return null;
+ }
+
+ public IImage getImageWithId(long id) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected int indexOrientation() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDateTaken() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMimeType() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDescription() {
+ return -1;
+ }
+
+ @Override
+ protected int indexId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexData() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLatitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexLongitude() {
+ return -1;
+ }
+
+ @Override
+ protected int indexMiniThumbId() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPicasaWeb() {
+ return -1;
+ }
+
+ @Override
+ protected int indexPrivate() {
+ return -1;
+ }
+
+ @Override
+ protected int indexTitle() {
+ return -1;
+ }
+
+ @Override
+ protected int indexDisplayName() {
+ return -1;
+ }
+
+ @Override
+ protected int indexThumbId() {
+ return -1;
+ }
+
+ private InputStream makeInputStream(Uri uri) {
+ InputStream input = null;
+ try {
+ input = mContentResolver.openInputStream(uri);
+ return input;
+ } catch (IOException ex) {
+ return null;
+ }
+ }
+
+ @Override
+ protected Bitmap makeBitmap(int targetWidthHeight, Uri uri, ParcelFileDescriptor pfdInput, BitmapFactory.Options options) {
+ Bitmap b = null;
+
+ try {
+ if (options == null)
+ options = new BitmapFactory.Options();
+ options.inSampleSize = 1;
+
+ if (targetWidthHeight != -1) {
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+
+ options.inSampleSize = computeSampleSize(options, targetWidthHeight);
+ options.inJustDecodeBounds = false;
+ }
+ b = BitmapFactory.decodeFileDescriptor(pfdInput.getFileDescriptor(), null, options);
+ if (VERBOSE) {
+ Log.v(TAG, "C: got bitmap " + b + " with sampleSize " + options.inSampleSize);
+ }
+ pfdInput.close();
+ } catch (IOException ex) {
+ if (VERBOSE) Log.v(TAG, "got io exception " + ex);
+ return null;
+ }
+ return b;
+ }
+ }
+
+ class ThreadSafeOutputStream extends OutputStream {
+ java.io.OutputStream mDelegateStream;
+ boolean mClosed;
+
+ public ThreadSafeOutputStream(OutputStream delegate) {
+ mDelegateStream = delegate;
+ }
+
+ @Override
+ synchronized public void close() throws IOException {
+ try {
+ mClosed = true;
+ mDelegateStream.close();
+ } catch (IOException ex) {
+
+ }
+ }
+
+ @Override
+ synchronized public void flush() throws IOException {
+ super.flush();
+ }
+
+ @Override
+ public void write(byte[] b, int offset, int length) throws IOException {
+ /*
+ mDelegateStream.write(b, offset, length);
+ return;
+ */
+ while (length > 0) {
+ synchronized (this) {
+ if (mClosed)
+ return;
+
+ int writeLength = Math.min(8192, length);
+ mDelegateStream.write(b, offset, writeLength);
+ offset += writeLength;
+ length -= writeLength;
+ }
+ }
+ }
+
+ @Override
+ synchronized public void write(int oneByte) throws IOException {
+ if (mClosed)
+ return;
+ mDelegateStream.write(oneByte);
+ }
+ }
+
+ /*
+ * How much quality to use when storing the thumbnail.
+ */
+ private static ImageManager sInstance = null;
+ private static final int MINI_THUMB_TARGET_SIZE = 96;
+ private static final int THUMBNAIL_TARGET_SIZE = 320;
+
+ private static final String[] THUMB_PROJECTION = new String[] {
+ BaseColumns._ID, // 0
+ Images.Thumbnails.IMAGE_ID, // 1
+ Images.Thumbnails.WIDTH,
+ Images.Thumbnails.HEIGHT
+ };
+
+ private static Uri sStorageURI = Images.Media.EXTERNAL_CONTENT_URI;
+
+ private static Uri sThumbURI = Images.Thumbnails.EXTERNAL_CONTENT_URI;
+ /**
+ * Returns an ImageList object that contains
+ * all of the images.
+ * @param cr
+ * @param location
+ * @param includeImages
+ * @param includeVideo
+ * @return the singleton ImageList
+ */
+ static final public int SORT_ASCENDING = 1;
+
+ static final public int SORT_DESCENDING = 2;
+
+ static final public int INCLUDE_IMAGES = (1 << 0);
+ static final public int INCLUDE_DRM_IMAGES = (1 << 1);
+ static final public int INCLUDE_VIDEOS = (1 << 2);
+
+ static public DataLocation getDefaultDataLocation() {
+ return DataLocation.EXTERNAL;
+ }
+ private static int indexOf(String [] array, String s) {
+ for (int i = 0; i < array.length; i++) {
+ if (array[i].equals(s)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Returns the singleton instance of the ImageManager.
+ * @return the ImageManager instance.
+ */
+ public static ImageManager instance() {
+ if (sInstance == null) {
+ sInstance = new ImageManager();
+ }
+ return sInstance;
+ }
+
+ static public byte [] miniThumbData(Bitmap source) {
+ if (source == null)
+ return null;
+
+ float scale;
+ if (source.getWidth() < source.getHeight()) {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getWidth();
+ } else {
+ scale = MINI_THUMB_TARGET_SIZE / (float)source.getHeight();
+ }
+ Matrix matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ Bitmap miniThumbnail = ImageLoader.transform(matrix, source,
+ MINI_THUMB_TARGET_SIZE, MINI_THUMB_TARGET_SIZE, false);
+
+ if (miniThumbnail != source) {
+ source.recycle();
+ }
+ java.io.ByteArrayOutputStream miniOutStream = new java.io.ByteArrayOutputStream();
+ miniThumbnail.compress(Bitmap.CompressFormat.JPEG, 75, miniOutStream);
+ miniThumbnail.recycle();
+
+ try {
+ miniOutStream.close();
+ byte [] data = miniOutStream.toByteArray();
+ return data;
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "got exception ex " + ex);
+ }
+ return null;
+ }
+
+ static Bitmap rotate(Bitmap b, int degrees) {
+ if (degrees != 0 && b != null) {
+ Matrix m = new Matrix();
+ m.setRotate(degrees, (float) b.getWidth() / 2, (float) b.getHeight() / 2);
+
+ Bitmap b2 = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ // TODO should recycle here but that needs more testing/verification
+// b.recycle();
+ b = b2;
+ }
+ return b;
+ }
+
+ public static int roundOrientation(int orientationInput) {
+ int orientation = orientationInput;
+ if (orientation == -1)
+ orientation = 0;
+
+ orientation = orientation % 360;
+ int retVal;
+ if (orientation < (0*90) + 45) {
+ retVal = 0;
+ } else if (orientation < (1*90) + 45) {
+ retVal = 90;
+ } else if (orientation < (2*90) + 45) {
+ retVal = 180;
+ } else if (orientation < (3*90) + 45) {
+ retVal = 270;
+ } else {
+ retVal = 0;
+ }
+
+ if (VERBOSE) Log.v(TAG, "map orientation " + orientationInput + " to " + retVal);
+ return retVal;
+ }
+
+ public Uri addImage(
+ final Context ctx,
+ final ContentResolver cr,
+ final String imageName,
+ final String description,
+ final long dateTaken,
+ final Location location,
+ final int orientation,
+ final String directory,
+ final String filename) {
+ ContentValues values = new ContentValues(7);
+ values.put(Images.Media.TITLE, imageName);
+ values.put(Images.Media.DISPLAY_NAME, imageName);
+ values.put(Images.Media.DESCRIPTION, description);
+ values.put(Images.Media.DATE_TAKEN, dateTaken);
+ values.put(Images.Media.MIME_TYPE, "image/jpeg");
+ values.put(Images.Media.ORIENTATION, orientation);
+
+ File parentFile = new File(directory);
+ String path = parentFile.toString().toLowerCase();
+ String name = parentFile.getName().toLowerCase();
+
+ values.put(Images.ImageColumns.BUCKET_ID, path.hashCode());
+ values.put(Images.ImageColumns.BUCKET_DISPLAY_NAME, name);
+ if (VERBOSE) Log.v(TAG, "addImage id is " + path.hashCode() + "; name " + name + "; path is " + path);
+
+ if (location != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "lat long " + location.getLatitude() + " / " + location.getLongitude());
+ }
+ values.put(Images.Media.LATITUDE, location.getLatitude());
+ values.put(Images.Media.LONGITUDE, location.getLongitude());
+ }
+
+ if (directory != null && filename != null) {
+ String value = directory + "/" + filename;
+ values.put("_data", value);
+ }
+
+ long t3 = System.currentTimeMillis();
+ Uri uri = cr.insert(sStorageURI, values);
+
+ // The line above will create a filename that ends in .jpg
+ // That filename is what will be handed to gmail when a user shares a photo.
+ // Gmail gets the name of the picture attachment from the "DISPLAY_NAME" field.
+ // Extract the filename and jam it into the display name.
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, Images.Media.DISPLAY_NAME, "_data" },
+ null,
+ null,
+ null);
+ if (c.moveToFirst()) {
+ String filePath = c.getString(2);
+ if (filePath != null) {
+ int pos = filePath.lastIndexOf("/");
+ if (pos >= 0) {
+ filePath = filePath.substring(pos + 1); // pick off the filename
+ c.updateString(1, filePath);
+ c.commitUpdates();
+ }
+ }
+ }
+ c.close();
+ return uri;
+ }
+
+ public IAddImage_cancelable storeImage(
+ final Uri uri,
+ final Context ctx,
+ final ContentResolver cr,
+ final int orientation,
+ final Bitmap source,
+ final byte [] jpegData) {
+ class AddImageCancelable extends BaseCancelable implements IAddImage_cancelable {
+ private IGetBoolean_cancelable mSaveImageCancelable;
+
+ public boolean doCancelWork() {
+ if (VERBOSE) {
+ Log.v(TAG, "calling AddImageCancelable.cancel() " + mSaveImageCancelable);
+ }
+
+ if (mSaveImageCancelable != null) {
+ mSaveImageCancelable.cancel();
+ }
+ return true;
+ }
+
+ public void get() {
+ if (source == null && jpegData == null) {
+ throw new IllegalArgumentException("source cannot be null");
+ }
+
+ try {
+ long t1 = System.currentTimeMillis();
+ synchronized (this) {
+ if (mCancel) {
+ throw new CanceledException();
+ }
+ }
+ long id = ContentUris.parseId(uri);
+
+ BaseImageList il = new ImageList(ctx, cr, sStorageURI, sThumbURI, SORT_ASCENDING, null);
+ ImageManager.Image image = new Image(id, 0, cr, il, il.getCount(), 0);
+ long t5 = System.currentTimeMillis();
+ Cursor c = cr.query(
+ uri,
+ new String [] { ImageColumns._ID, ImageColumns.MINI_THUMB_MAGIC, "_data" },
+ null,
+ null,
+ null);
+ c.moveToPosition(0);
+
+ synchronized (this) {
+ checkCanceled();
+ mSaveImageCancelable = image.saveImageContents(source, jpegData, orientation, true, c);
+ }
+
+ if (mSaveImageCancelable.get()) {
+ long t6 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "saveImageContents took " + (t6-t5));
+ if (VERBOSE) Log.v(TAG, "updating new picture with id " + id);
+ c.updateLong(1, id);
+ c.commitUpdates();
+ c.close();
+ long t7 = System.currentTimeMillis();
+ if (VERBOSE) Log.v(TAG, "commit updates to save mini thumb took " + (t7-t6));
+ }
+ else {
+ c.close();
+ throw new CanceledException();
+ }
+ } catch (CanceledException ex) {
+ if (VERBOSE) {
+ Log.v(TAG, "caught CanceledException");
+ }
+ if (uri != null) {
+ if (VERBOSE) {
+ Log.v(TAG, "canceled... cleaning up this uri: " + uri);
+ }
+ cr.delete(uri, null, null);
+ }
+ acknowledgeCancel();
+ }
+ }
+ }
+ return new AddImageCancelable();
+ }
+
+ static public IImageList makeImageList(Uri uri, Context ctx, int sort) {
+ ContentResolver cr = ctx.getContentResolver();
+ String uriString = (uri != null) ? uri.toString() : "";
+ // TODO we need to figure out whether we're viewing
+ // DRM images in a better way. Is there a constant
+ // for content://drm somewhere??
+ IImageList imageList;
+
+ if (uriString.startsWith("content://drm")) {
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_DRM_IMAGES,
+ sort);
+ } else if (!uriString.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
+ && !uriString.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString())) {
+ imageList = ImageManager.instance().new SingleImageList(cr, uri);
+ } else {
+ String bucketId = uri.getQueryParameter("bucketId");
+ if (VERBOSE) Log.v(TAG, "bucketId is " + bucketId);
+ imageList = ImageManager.instance().allImages(
+ ctx,
+ cr,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES,
+ sort,
+ bucketId);
+ }
+ return imageList;
+ }
+
+ public IImageList emptyImageList() {
+ return
+ new IImageList() {
+ public void checkThumbnails(com.android.camera.ImageManager.IImageList.ThumbCheckCallback cb) {
+ }
+
+ public void commitChanges() {
+ }
+
+ public void deactivate() {
+ }
+
+ public HashMap<String, String> getBucketIds() {
+ return new HashMap<String,String>();
+ }
+
+ public int getCount() {
+ return 0;
+ }
+
+ public IImage getImageAt(int i) {
+ return null;
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ return null;
+ }
+
+ public boolean removeImage(IImage image) {
+ return false;
+ }
+
+ public void removeImageAt(int i) {
+ }
+
+ public void removeOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback) {
+ }
+
+ public void setOnChangeListener(com.android.camera.ImageManager.IImageList.OnChange changeCallback, Handler h) {
+ }
+
+ };
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort) {
+ return allImages(ctx, cr, location, inclusion, sort, null, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId) {
+ return allImages(ctx, cr, location, inclusion, sort, bucketId, null);
+ }
+
+ public IImageList allImages(Context ctx, ContentResolver cr, DataLocation location, int inclusion, int sort, String bucketId, Uri specificImageUri) {
+ if (VERBOSE) {
+ Log.v(TAG, "allImages " + location + " " + ((inclusion&INCLUDE_IMAGES)!=0) + " + v=" + ((inclusion&INCLUDE_VIDEOS)!=0));
+ }
+
+ if (cr == null) {
+ return null;
+ } else {
+ // false ==> don't require write access
+ boolean haveSdCard = hasStorage(false);
+
+ if (true) {
+ // use this code to merge videos and stills into the same list
+ ArrayList<IImageList> l = new ArrayList<IImageList>();
+
+ if (VERBOSE) {
+ Log.v(TAG, "initializing ... haveSdCard == " + haveSdCard + "; inclusion is " + String.format("%x", inclusion));
+ }
+ if (specificImageUri != null) {
+ try {
+ if (specificImageUri.getScheme().equalsIgnoreCase("content"))
+ l.add(new ImageList(ctx, cr, specificImageUri, sThumbURI, sort, bucketId));
+ else
+ l.add(new SingleImageList(cr, specificImageUri));
+ } catch (UnsupportedOperationException ex) {
+ }
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ if (location == DataLocation.INTERNAL || location == DataLocation.ALL) {
+ if ((inclusion & INCLUDE_IMAGES) != 0) {
+ try {
+ l.add(new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ if ((inclusion & INCLUDE_DRM_IMAGES) != 0) {
+ try {
+ l.add(new DrmImageList(ctx, cr, DrmStore.Images.CONTENT_URI, sort, bucketId));
+ } catch (UnsupportedOperationException ex) {
+ }
+ }
+ }
+ }
+
+ IImageList [] imageList = l.toArray(new IImageList[l.size()]);
+ return new ImageListUber(imageList, sort);
+ } else {
+ if (haveSdCard && location != DataLocation.INTERNAL) {
+ return new ImageList(ctx, cr, sStorageURI, sThumbURI, sort, bucketId);
+ } else {
+ return new ImageList(ctx, cr, Images.Media.INTERNAL_CONTENT_URI,
+ Images.Thumbnails.INTERNAL_CONTENT_URI, sort, bucketId);
+ }
+ }
+ }
+ }
+
+ // Create a temporary file to see whether a volume is really writeable. It's important not to
+ // put it in the root directory which may have a limit on the number of files.
+ static private boolean checkFsWritable() {
+ String directoryName = Environment.getExternalStorageDirectory().toString() + "/dcim";
+ File directory = new File(directoryName);
+ if (!directory.isDirectory()) {
+ if (!directory.mkdirs()) {
+ return false;
+ }
+ }
+ File f = new File(directoryName, ".probe");
+ try {
+ // Remove stale file if any
+ if (f.exists()) {
+ f.delete();
+ }
+ if (!f.createNewFile())
+ return false;
+ f.delete();
+ return true;
+ } catch (IOException ex) {
+ return false;
+ }
+ }
+
+ static public boolean hasStorage() {
+ return hasStorage(true);
+ }
+
+ static public boolean hasStorage(boolean requireWriteAccess) {
+ String state = Environment.getExternalStorageState();
+ if (VERBOSE) Log.v(TAG, "state is " + state);
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ if (requireWriteAccess) {
+ boolean writable = checkFsWritable();
+ if (VERBOSE) Log.v(TAG, "writable is " + writable);
+ return writable;
+ } else {
+ return true;
+ }
+ } else if (!requireWriteAccess && Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ return true;
+ }
+ return false;
+ }
+
+ public static Cursor query(Context context, Uri uri, String[] projection,
+ String selection, String[] selectionArgs, String sortOrder) {
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ if (resolver == null) {
+ return null;
+ }
+ return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ }
+
+ }
+
+ public static boolean isMediaScannerScanning(Context context) {
+ boolean result = false;
+ Cursor cursor = query(context, MediaStore.getMediaScannerUri(),
+ new String [] { MediaStore.MEDIA_SCANNER_VOLUME }, null, null, null);
+ if (cursor != null) {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ result = "external".equals(cursor.getString(0));
+ }
+ cursor.close();
+ }
+
+ if (VERBOSE)
+ Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>> isMediaScannerScanning returning " + result);
+ return result;
+ }
+}
diff --git a/src/com/android/camera/ImageViewTouchBase.java b/src/com/android/camera/ImageViewTouchBase.java
new file mode 100644
index 0000000..9993373
--- /dev/null
+++ b/src/com/android/camera/ImageViewTouchBase.java
@@ -0,0 +1,547 @@
+package com.android.camera;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.widget.ImageView;
+
+abstract public class ImageViewTouchBase extends ImageView {
+ private static final String TAG = "ImageViewTouchBase";
+
+ // if we're animating these images, it may be faster to cache the image
+ // at its target size first. to do this set this variable to true.
+ // currently we're not animating images, so we don't need to do this
+ // extra work.
+ private final boolean USE_PERFECT_FIT_OPTIMIZATION = false;
+
+ // This is the base transformation which is used to show the image
+ // initially. The current computation for this shows the image in
+ // it's entirety, letterboxing as needed. One could chose to
+ // show the image as cropped instead.
+ //
+ // This matrix is recomputed when we go from the thumbnail image to
+ // the full size image.
+ protected Matrix mBaseMatrix = new Matrix();
+
+ // This is the supplementary transformation which reflects what
+ // the user has done in terms of zooming and panning.
+ //
+ // This matrix remains the same when we go from the thumbnail image
+ // to the full size image.
+ protected Matrix mSuppMatrix = new Matrix();
+
+ // This is the final matrix which is computed as the concatentation
+ // of the base matrix and the supplementary matrix.
+ private Matrix mDisplayMatrix = new Matrix();
+
+ // Temporary buffer used for getting the values out of a matrix.
+ private float[] mMatrixValues = new float[9];
+
+ // The current bitmap being displayed.
+ protected Bitmap mBitmapDisplayed;
+
+ // The thumbnail bitmap.
+ protected Bitmap mThumbBitmap;
+
+ // The full size bitmap which should be used once we start zooming.
+ private Bitmap mFullBitmap;
+
+ // The bitmap which is exactly sized to what we need. The decoded bitmap is
+ // drawn into the mPerfectFitBitmap so that animation is faster.
+ protected Bitmap mPerfectFitBitmap;
+
+ // True if the image is the thumbnail.
+ protected boolean mBitmapIsThumbnail;
+
+ // True if the user is zooming -- use the full size image
+ protected boolean mIsZooming;
+
+ // Paint to use to clear the "mPerfectFitBitmap"
+ protected Paint mPaint = new Paint();
+
+ static boolean sNewZoomControl = false;
+
+ int mThisWidth = -1, mThisHeight = -1;
+
+ float mMaxZoom;
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mThisWidth = right - left;
+ mThisHeight = bottom - top;
+ Runnable r = mOnLayoutRunnable;
+ if (r != null) {
+ mOnLayoutRunnable = null;
+ r.run();
+ }
+ if (mBitmapDisplayed != null) {
+ setBaseMatrix(mBitmapDisplayed, mBaseMatrix);
+ setImageMatrix(getImageViewMatrix());
+ }
+ }
+
+ protected Handler mHandler = new Handler();
+
+ protected int mLastXTouchPos;
+ protected int mLastYTouchPos;
+
+ protected boolean doesScrolling() {
+ return true;
+ }
+
+ // Translate a given point through a given matrix.
+ static private void translatePoint(Matrix matrix, float [] xy) {
+ matrix.mapPoints(xy);
+ }
+
+ // Return the mapped x coordinate through the matrix.
+ static int mapXPoint(Matrix matrix, int point) {
+ // Matrix's mapPoints takes an array of x/y coordinates.
+ // That's why we have to allocte an array of length two
+ // even though we don't use the y coordinate.
+ float [] xy = new float[2];
+ xy[0] = point;
+ xy[1] = 0F;
+ matrix.mapPoints(xy);
+ return (int) xy[0];
+ }
+
+ @Override
+ public void setImageBitmap(Bitmap bitmap) {
+ throw new NullPointerException();
+ }
+
+ public void setImageBitmap(Bitmap bitmap, boolean isThumbnail) {
+ super.setImageBitmap(bitmap);
+ Drawable d = getDrawable();
+ if (d != null)
+ d.setDither(true);
+ mBitmapDisplayed = bitmap;
+ mBitmapIsThumbnail = isThumbnail;
+ }
+
+ protected boolean usePerfectFitBitmap() {
+ return USE_PERFECT_FIT_OPTIMIZATION && !mIsZooming;
+ }
+
+ public void recycleBitmaps() {
+ if (mFullBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mFullBitmap " + mFullBitmap + "; this == " + this.hashCode());
+ mFullBitmap.recycle();
+ mFullBitmap = null;
+ }
+ if (mThumbBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mThumbBitmap" + mThumbBitmap + "; this == " + this.hashCode());
+ mThumbBitmap.recycle();
+ mThumbBitmap = null;
+ }
+
+ // mBitmapDisplayed is either mPerfectFitBitmap or mFullBitmap (in the case of zooming)
+ setImageBitmap(null, true);
+ }
+
+ public void clear() {
+ mBitmapDisplayed = null;
+ recycleBitmaps();
+ }
+
+ private Runnable mOnLayoutRunnable = null;
+
+ public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp, final boolean isThumb) {
+ if ((bitmap != null) && (bitmap == mPerfectFitBitmap)) {
+ // TODO: this should be removed in production
+ throw new IllegalArgumentException("bitmap must not be mPerfectFitBitmap");
+ }
+
+ final int viewWidth = getWidth();
+ final int viewHeight = getHeight();
+
+ if (viewWidth <= 0) {
+ mOnLayoutRunnable = new Runnable() {
+ public void run() {
+ setImageBitmapResetBase(bitmap, resetSupp, isThumb);
+ }
+ };
+ return;
+ }
+
+ if (isThumb && mThumbBitmap != bitmap) {
+ if (mThumbBitmap != null) {
+ mThumbBitmap.recycle();
+ }
+ mThumbBitmap = bitmap;
+ } else if (!isThumb && mFullBitmap != bitmap) {
+ if (mFullBitmap != null) {
+ mFullBitmap.recycle();
+ }
+ mFullBitmap = bitmap;
+ }
+ mBitmapIsThumbnail = isThumb;
+
+ if (bitmap != null) {
+ if (!usePerfectFitBitmap()) {
+ setScaleType(ImageView.ScaleType.MATRIX);
+ setBaseMatrix(bitmap, mBaseMatrix);
+ setImageBitmap(bitmap, isThumb);
+ } else {
+ Matrix matrix = new Matrix();
+ setBaseMatrix(bitmap, matrix);
+ if ((mPerfectFitBitmap == null) ||
+ mPerfectFitBitmap.getWidth() != mThisWidth ||
+ mPerfectFitBitmap.getHeight() != mThisHeight) {
+ if (mPerfectFitBitmap != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "recycling mPerfectFitBitmap " + mPerfectFitBitmap.hashCode());
+ mPerfectFitBitmap.recycle();
+ }
+ mPerfectFitBitmap = Bitmap.createBitmap(mThisWidth, mThisHeight, Bitmap.Config.RGB_565);
+ }
+ Canvas canvas = new Canvas(mPerfectFitBitmap);
+ // clear the bitmap which may be bigger than the image and
+ // contain the the previous image.
+ canvas.drawColor(0xFF000000);
+
+ final int bw = bitmap.getWidth();
+ final int bh = bitmap.getHeight();
+ final float widthScale = Math.min(viewWidth / (float)bw, 1.0f);
+ final float heightScale = Math.min(viewHeight/ (float)bh, 1.0f);
+ int translateX, translateY;
+ if (widthScale > heightScale) {
+ translateX = (int)((viewWidth -(float)bw*heightScale)*0.5f);
+ translateY = (int)((viewHeight-(float)bh*heightScale)*0.5f);
+ } else {
+ translateX = (int)((viewWidth -(float)bw*widthScale)*0.5f);
+ translateY = (int)((viewHeight-(float)bh*widthScale)*0.5f);
+ }
+
+ android.graphics.Rect src = new android.graphics.Rect(0, 0, bw, bh);
+ android.graphics.Rect dst = new android.graphics.Rect(
+ translateX, translateY,
+ mThisWidth - translateX, mThisHeight - translateY);
+ canvas.drawBitmap(bitmap, src, dst, mPaint);
+
+ setImageBitmap(mPerfectFitBitmap, isThumb);
+ setScaleType(ImageView.ScaleType.MATRIX);
+ setImageMatrix(null);
+ }
+ } else {
+ mBaseMatrix.reset();
+ setImageBitmap(null, isThumb);
+ }
+
+ if (resetSupp)
+ mSuppMatrix.reset();
+ setImageMatrix(getImageViewMatrix());
+ mMaxZoom = maxZoom();
+ }
+
+ // Center as much as possible in one or both axis. Centering is
+ // defined as follows: if the image is scaled down below the
+ // view's dimensions then center it (literally). If the image
+ // is scaled larger than the view and is translated out of view
+ // then translate it back into view (i.e. eliminate black bars).
+ protected void center(boolean vertical, boolean horizontal, boolean animate) {
+ if (mBitmapDisplayed == null)
+ return;
+
+ Matrix m = getImageViewMatrix();
+
+ float [] topLeft = new float[] { 0, 0 };
+ float [] botRight = new float[] { mBitmapDisplayed.getWidth(), mBitmapDisplayed.getHeight() };
+
+ translatePoint(m, topLeft);
+ translatePoint(m, botRight);
+
+ float height = botRight[1] - topLeft[1];
+ float width = botRight[0] - topLeft[0];
+
+ float deltaX = 0, deltaY = 0;
+
+ if (vertical) {
+ int viewHeight = getHeight();
+ if (height < viewHeight) {
+ deltaY = (viewHeight - height)/2 - topLeft[1];
+ } else if (topLeft[1] > 0) {
+ deltaY = -topLeft[1];
+ } else if (botRight[1] < viewHeight) {
+ deltaY = getHeight() - botRight[1];
+ }
+ }
+
+ if (horizontal) {
+ int viewWidth = getWidth();
+ if (width < viewWidth) {
+ deltaX = (viewWidth - width)/2 - topLeft[0];
+ } else if (topLeft[0] > 0) {
+ deltaX = -topLeft[0];
+ } else if (botRight[0] < viewWidth) {
+ deltaX = viewWidth - botRight[0];
+ }
+ }
+
+ postTranslate(deltaX, deltaY);
+ if (animate) {
+ Animation a = new TranslateAnimation(-deltaX, 0, -deltaY, 0);
+ a.setStartTime(SystemClock.elapsedRealtime());
+ a.setDuration(250);
+ setAnimation(a);
+ }
+ setImageMatrix(getImageViewMatrix());
+ }
+
+ public void copyFrom(ImageViewTouchBase other) {
+ mSuppMatrix.set(other.mSuppMatrix);
+ mBaseMatrix.set(other.mBaseMatrix);
+
+ if (mThumbBitmap != null)
+ mThumbBitmap.recycle();
+
+ if (mFullBitmap != null)
+ mFullBitmap.recycle();
+
+ // copy the data
+ mThumbBitmap = other.mThumbBitmap;
+ mFullBitmap = null;
+
+ if (other.mFullBitmap != null)
+ other.mFullBitmap.recycle();
+
+ // transfer "ownership"
+ other.mThumbBitmap = null;
+ other.mFullBitmap = null;
+ other.mBitmapIsThumbnail = true;
+
+ setImageMatrix(other.getImageMatrix());
+ setScaleType(other.getScaleType());
+
+ setImageBitmapResetBase(mThumbBitmap, true, true);
+ }
+
+ @Override
+ public void setImageDrawable(android.graphics.drawable.Drawable d) {
+ super.setImageDrawable(d);
+ }
+
+ public ImageViewTouchBase(Context context) {
+ super(context);
+ init();
+ }
+
+ public ImageViewTouchBase(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ setScaleType(ImageView.ScaleType.MATRIX);
+ mPaint.setDither(true);
+ mPaint.setFilterBitmap(true);
+ }
+
+ protected float getValue(Matrix matrix, int whichValue) {
+ matrix.getValues(mMatrixValues);
+ return mMatrixValues[whichValue];
+ }
+
+ // Get the scale factor out of the matrix.
+ protected float getScale(Matrix matrix) {
+ return getValue(matrix, Matrix.MSCALE_X);
+ }
+
+ protected float getScale() {
+ return getScale(mSuppMatrix);
+ }
+
+ protected float getTranslateX() {
+ return getValue(mSuppMatrix, Matrix.MTRANS_X);
+ }
+
+ protected float getTranslateY() {
+ return getValue(mSuppMatrix, Matrix.MTRANS_Y);
+ }
+
+ // Setup the base matrix so that the image is centered and scaled properly.
+ private void setBaseMatrix(Bitmap bitmap, Matrix matrix) {
+ float viewWidth = getWidth();
+ float viewHeight = getHeight();
+
+ matrix.reset();
+ float widthScale = Math.min(viewWidth / (float)bitmap.getWidth(), 1.0f);
+ float heightScale = Math.min(viewHeight / (float)bitmap.getHeight(), 1.0f);
+ float scale;
+ if (widthScale > heightScale) {
+ scale = heightScale;
+ } else {
+ scale = widthScale;
+ }
+ matrix.setScale(scale, scale);
+ matrix.postTranslate(
+ (viewWidth - ((float)bitmap.getWidth() * scale))/2F,
+ (viewHeight - ((float)bitmap.getHeight() * scale))/2F);
+ }
+
+ // Combine the base matrix and the supp matrix to make the final matrix.
+ protected Matrix getImageViewMatrix() {
+ mDisplayMatrix.set(mBaseMatrix);
+ mDisplayMatrix.postConcat(mSuppMatrix);
+ return mDisplayMatrix;
+ }
+
+ private void onZoom() {
+ mIsZooming = true;
+ if (mFullBitmap != null && mFullBitmap != mBitmapDisplayed) {
+ setImageBitmapResetBase(mFullBitmap, false, mBitmapIsThumbnail);
+ }
+ }
+
+ private String describe(Bitmap b) {
+ StringBuilder sb = new StringBuilder();
+ if (b == null) {
+ sb.append("NULL");
+ } else if (b.isRecycled()) {
+ sb.append(String.format("%08x: RECYCLED", b.hashCode()));
+ } else {
+ sb.append(String.format("%08x: LIVE", b.hashCode()));
+ sb.append(String.format("%d x %d (size == %d)", b.getWidth(), b.getHeight(), b.getWidth()*b.getHeight()*2));
+ }
+ return sb.toString();
+ }
+
+ public void dump() {
+ if (Config.LOGV) {
+ Log.v(TAG, "dump ImageViewTouchBase " + this);
+ Log.v(TAG, "... mBitmapDisplayed = " + describe(mBitmapDisplayed));
+ Log.v(TAG, "... mThumbBitmap = " + describe(mThumbBitmap));
+ Log.v(TAG, "... mFullBitmap = " + describe(mFullBitmap));
+ Log.v(TAG, "... mPerfectFitBitmap = " + describe(mPerfectFitBitmap));
+ Log.v(TAG, "... mIsThumb = " + mBitmapIsThumbnail);
+ }
+ }
+
+ static final float sPanRate = 7;
+ static final float sScaleRate = 1.05F;
+
+ // Sets the maximum zoom, which is a scale relative to the base matrix. It is calculated to show
+ // the image at 400% zoom regardless of screen or image orientation. If in the future we decode
+ // the full 3 megapixel image, rather than the current 1024x768, this should be changed down to
+ // 200%.
+ protected float maxZoom() {
+ if (mBitmapDisplayed == null)
+ return 1F;
+
+ float fw = (float) mBitmapDisplayed.getWidth() / (float)mThisWidth;
+ float fh = (float) mBitmapDisplayed.getHeight() / (float)mThisHeight;
+ float max = Math.max(fw, fh) * 4;
+// Log.v(TAG, "Bitmap " + mBitmapDisplayed.getWidth() + "x" + mBitmapDisplayed.getHeight() +
+// " view " + mThisWidth + "x" + mThisHeight + " max zoom " + max);
+ return max;
+ }
+
+ protected void zoomTo(float scale, float centerX, float centerY) {
+ if (scale > mMaxZoom) {
+ scale = mMaxZoom;
+ }
+ onZoom();
+
+ float oldScale = getScale();
+ float deltaScale = scale / oldScale;
+
+ mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
+ setImageMatrix(getImageViewMatrix());
+ center(true, true, false);
+ }
+
+ protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) {
+ final float incrementPerMs = (scale - getScale()) / durationMs;
+ final float oldScale = getScale();
+ final long startTime = System.currentTimeMillis();
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ long now = System.currentTimeMillis();
+ float currentMs = Math.min(durationMs, (float)(now - startTime));
+ float target = oldScale + (incrementPerMs * currentMs);
+ zoomTo(target, centerX, centerY);
+
+ if (currentMs < durationMs) {
+ mHandler.post(this);
+ }
+ }
+ });
+ }
+
+ protected void zoomTo(float scale) {
+ float width = getWidth();
+ float height = getHeight();
+
+ zoomTo(scale, width/2F, height/2F);
+ }
+
+ protected void zoomIn() {
+ zoomIn(sScaleRate);
+ }
+
+ protected void zoomOut() {
+ zoomOut(sScaleRate);
+ }
+
+ protected void zoomIn(float rate) {
+ if (getScale() >= mMaxZoom) {
+ return; // Don't let the user zoom into the molecular level.
+ }
+ if (mBitmapDisplayed == null) {
+ return;
+ }
+ float width = getWidth();
+ float height = getHeight();
+
+ mSuppMatrix.postScale(rate, rate, width/2F, height/2F);
+ setImageMatrix(getImageViewMatrix());
+
+ onZoom();
+ }
+
+ protected void zoomOut(float rate) {
+ if (mBitmapDisplayed == null) {
+ return;
+ }
+
+ float width = getWidth();
+ float height = getHeight();
+
+ Matrix tmp = new Matrix(mSuppMatrix);
+ tmp.postScale(1F/sScaleRate, 1F/sScaleRate, width/2F, height/2F);
+ if (getScale(tmp) < 1F) {
+ mSuppMatrix.setScale(1F, 1F, width/2F, height/2F);
+ } else {
+ mSuppMatrix.postScale(1F/rate, 1F/rate, width/2F, height/2F);
+ }
+ setImageMatrix(getImageViewMatrix());
+ center(true, true, false);
+
+ onZoom();
+ }
+
+ protected void postTranslate(float dx, float dy) {
+ mSuppMatrix.postTranslate(dx, dy);
+ }
+
+ protected void panBy(float dx, float dy) {
+ postTranslate(dx, dy);
+ setImageMatrix(getImageViewMatrix());
+ }
+}
+
diff --git a/src/com/android/camera/MenuHelper.java b/src/com/android/camera/MenuHelper.java
new file mode 100644
index 0000000..033fc9c
--- /dev/null
+++ b/src/com/android/camera/MenuHelper.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2008 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.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Config;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+public class MenuHelper {
+ static private final String TAG = "MenuHelper";
+
+ static public final int GENERIC_ITEM = 1;
+ static public final int IMAGE_SAVING_ITEM = 2;
+ static public final int VIDEO_SAVING_ITEM = 3;
+ static public final int IMAGE_MODE_ITEM = 4;
+ static public final int VIDEO_MODE_ITEM = 5;
+ static public final int MENU_ITEM_MAX = 5;
+
+ static public final int INCLUDE_ALL = 0xFFFFFFFF;
+ static public final int INCLUDE_VIEWPLAY_MENU = (1 << 0);
+ static public final int INCLUDE_SHARE_MENU = (1 << 1);
+ static public final int INCLUDE_SET_MENU = (1 << 2);
+ static public final int INCLUDE_CROP_MENU = (1 << 3);
+ static public final int INCLUDE_DELETE_MENU = (1 << 4);
+ static public final int INCLUDE_ROTATE_MENU = (1 << 5);
+ static public final int INCLUDE_DETAILS_MENU = (1 << 5);
+
+ static public final int MENU_IMAGE_SHARE = 10;
+ static public final int MENU_IMAGE_SHARE_EMAIL = 11;
+ static public final int MENU_IMAGE_SHARE_MMS = 12;
+ static public final int MENU_IMAGE_SHARE_PICASA =13;
+ static public final int MENU_IMAGE_SET = 14;
+ static public final int MENU_IMAGE_SET_WALLPAPER = 15;
+ static public final int MENU_IMAGE_SET_CONTACT = 16;
+ static public final int MENU_IMAGE_SET_MYFAVE = 17;
+ static public final int MENU_IMAGE_CROP = 18;
+ static public final int MENU_IMAGE_ROTATE = 19;
+ static public final int MENU_IMAGE_ROTATE_LEFT = 20;
+ static public final int MENU_IMAGE_ROTATE_RIGHT = 21;
+ static public final int MENU_IMAGE_TOSS = 22;
+ static public final int MENU_VIDEO_PLAY = 23;
+ static public final int MENU_VIDEO_SHARE = 24;
+ static public final int MENU_VIDEO_SHARE_MMS = 25;
+ static public final int MENU_VIDEO_SHARE_YOUTUBE = 26;
+ static public final int MENU_VIDEO_TOSS = 27;
+ static public final int MENU_IMAGE_SHARE_PICASA_ALL =28;
+
+ public interface MenuItemsResult {
+ public void gettingReadyToOpen(Menu menu, ImageManager.IImage image);
+ public void aboutToCall(MenuItem item, ImageManager.IImage image);
+ }
+
+ public interface MenuInvoker {
+ public void run(MenuCallback r);
+ }
+
+ public interface MenuCallback {
+ public void run(Uri uri, ImageManager.IImage image);
+ }
+
+ static MenuItemsResult addImageMenuItems(
+ Menu menu,
+ int inclusions,
+ final Activity activity,
+ final Handler handler,
+ final Runnable onDelete,
+ final MenuInvoker onInvoke) {
+ final ArrayList<MenuItem> requiresWriteAccessItems = new ArrayList<MenuItem>();
+ final ArrayList<MenuItem> requiresNoDrmAccessItems = new ArrayList<MenuItem>();
+ if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
+ if (Config.LOGV)
+ Log.v(TAG, ">>>>> add share");
+ MenuItem item = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_SHARE, 10,
+ R.string.camera_share).setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null)
+ return;
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType(image.getMimeType());
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ activity.startActivity(Intent.createChooser(intent,
+ activity.getText(R.string.sendImage)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(activity, R.string.no_way_to_share_image, Toast.LENGTH_SHORT).show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_share);
+ requiresNoDrmAccessItems.add(item);
+ }
+
+ if ((inclusions & INCLUDE_ROTATE_MENU) != 0) {
+ SubMenu rotateSubmenu = menu.addSubMenu(IMAGE_SAVING_ITEM, MENU_IMAGE_ROTATE,
+ 40, R.string.rotate).setIcon(android.R.drawable.ic_menu_rotate);
+ // Don't show the rotate submenu if the item at hand is read only
+ // since the items within the submenu won't be shown anyway. This is
+ // really a framework bug in that it shouldn't show the submenu if
+ // the submenu has no visible items.
+ requiresWriteAccessItems.add(rotateSubmenu.getItem());
+ if (rotateSubmenu != null) {
+ requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_LEFT, 50, R.string.rotate_left).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null || image.isReadonly())
+ return;
+ image.rotateImageBy(-90);
+ }
+ });
+ return true;
+ }
+ }).setAlphabeticShortcut('l'));
+ requiresWriteAccessItems.add(rotateSubmenu.add(0, MENU_IMAGE_ROTATE_RIGHT, 60, R.string.rotate_right).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null || image.isReadonly())
+ return;
+
+ image.rotateImageBy(90);
+ }
+ });
+ return true;
+ }
+ }).setAlphabeticShortcut('r'));
+ }
+ }
+
+ if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
+ MenuItem deleteItem = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_TOSS, 70, R.string.camera_toss);
+ requiresWriteAccessItems.add(deleteItem);
+ deleteItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ deletePhoto(activity, onDelete);
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('d')
+ .setIcon(android.R.drawable.ic_menu_delete);
+ }
+
+ if ((inclusions & INCLUDE_CROP_MENU) != 0) {
+ MenuItem autoCrop = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_CROP, 73,
+ R.string.camera_crop).setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (u == null)
+ return;
+
+ Intent cropIntent = new Intent();
+ cropIntent.setClass(activity, CropImage.class);
+ cropIntent.setData(u);
+ activity.startActivity(cropIntent);
+ }
+ });
+ return true;
+ }
+ });
+ autoCrop.setIcon(android.R.drawable.ic_menu_crop);
+ requiresWriteAccessItems.add(autoCrop);
+ }
+
+ if ((inclusions & INCLUDE_SET_MENU) != 0) {
+ MenuItem setMenu = menu.add(IMAGE_SAVING_ITEM, MENU_IMAGE_SET, 75, R.string.camera_set);
+ setMenu.setIcon(android.R.drawable.ic_menu_set_as);
+
+ setMenu.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (u == null || image == null)
+ return;
+
+ if (Config.LOGV)
+ Log.v(TAG, "in callback u is " + u + "; mime type is " + image.getMimeType());
+ Intent intent = new Intent(Intent.ACTION_ATTACH_DATA);
+ intent.setDataAndType(u, image.getMimeType());
+ intent.putExtra("mimeType", image.getMimeType());
+ activity.startActivity(Intent.createChooser(intent, activity.getText(R.string.setImage)));
+ }
+ });
+ return true;
+ }
+ });
+ }
+
+ if ((inclusions & INCLUDE_DETAILS_MENU) != 0) {
+ MenuItem detailsMenu = menu.add(0, 0, 80, R.string.details).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ onInvoke.run(new MenuCallback() {
+ public void run(Uri u, ImageManager.IImage image) {
+ if (image == null)
+ return;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+
+ final View d = View.inflate(activity, R.layout.detailsview, null);
+
+ ImageView imageView = (ImageView) d.findViewById(R.id.details_thumbnail_image);
+ imageView.setImageBitmap(image.miniThumbBitmap());
+
+ TextView textView = (TextView) d.findViewById(R.id.details_image_title);
+ textView.setText(image.getDisplayName());
+
+ java.io.InputStream data = image.fullSizeImageData();
+ String lengthString = "";
+ try {
+ long length = data.available();
+ lengthString =
+ android.content.Formatter.formatFileSize(activity, length);
+ data.close();
+ } catch (java.io.IOException ex) {
+
+ } finally {
+ }
+ ((TextView)d.findViewById(R.id.details_attrname_1)).setText(R.string.details_file_size);
+ ((TextView)d.findViewById(R.id.details_attrvalu_1)).setText(lengthString);
+
+ String dimensionsString = String.valueOf(image.getWidth() + " X " + image.getHeight());
+ ((TextView)d.findViewById(R.id.details_attrname_2)).setText(R.string.details_image_resolution);
+ ((TextView)d.findViewById(R.id.details_attrvalu_2)).setText(dimensionsString);
+
+ String dateString = "";
+ long dateTaken = image.getDateTaken();
+ if (dateTaken != 0) {
+ java.util.Date date = new java.util.Date(image.getDateTaken());
+ java.text.SimpleDateFormat dateFormat = new java.text.SimpleDateFormat();
+ dateString = dateFormat.format(date);
+
+ ((TextView)d.findViewById(R.id.details_attrname_3)).setText(R.string.details_date_taken);
+ ((TextView)d.findViewById(R.id.details_attrvalu_3)).setText(dateString);
+ } else {
+ d.findViewById(R.id.details_daterow).setVisibility(View.GONE);
+ }
+
+
+ builder.setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(R.string.details_panel_title)
+ .setView(d)
+ .show();
+
+ }
+ });
+ return true;
+ }
+ });
+ detailsMenu.setIcon(R.drawable.ic_menu_view_details);
+ }
+
+ return new MenuItemsResult() {
+ public void gettingReadyToOpen(Menu menu, ImageManager.IImage image) {
+ // protect against null here. this isn't strictly speaking required
+ // but if a client app isn't handling sdcard removal properly it
+ // could happen
+ if (image == null) {
+ return;
+ }
+ boolean readOnly = image.isReadonly();
+ boolean isDrm = image.isDrm();
+ if (Config.LOGV)
+ Log.v(TAG, "readOnly: " + readOnly + "; drm: " + isDrm);
+ for (MenuItem item: requiresWriteAccessItems) {
+ if (Config.LOGV)
+ Log.v(TAG, "item is " + item.toString());
+ item.setVisible(!readOnly);
+ item.setEnabled(!readOnly);
+ }
+ for (MenuItem item: requiresNoDrmAccessItems) {
+ if (Config.LOGV)
+ Log.v(TAG, "item is " + item.toString());
+ item.setVisible(!isDrm);
+ item.setEnabled(!isDrm);
+ }
+ }
+ public void aboutToCall(MenuItem menu, ImageManager.IImage image) {
+ }
+ };
+ }
+
+ static MenuItemsResult addVideoMenuItems(
+ Menu menu,
+ int inclusions,
+ final Activity activity,
+ final Handler handler,
+ final SelectedImageGetter mGetter,
+ final Runnable onDelete,
+ final Runnable preWork,
+ final Runnable postWork) {
+
+ if ((inclusions & INCLUDE_VIEWPLAY_MENU) != 0) {
+ menu.add(VIDEO_SAVING_ITEM, MENU_VIDEO_PLAY, 0, R.string.video_play).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ if (preWork != null)
+ preWork.run();
+
+ Intent intent = new Intent(Intent.ACTION_VIEW, mGetter.getCurrentImageUri());
+ activity.startActivity(intent);
+
+ // don't do the postWork since we're launching another activity
+ return true;
+ }
+ });
+ }
+
+ if ((inclusions & INCLUDE_SHARE_MENU) != 0) {
+ MenuItem item = menu.add(VIDEO_SAVING_ITEM, 0, 0, R.string.camera_share).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Uri u = mGetter.getCurrentImageUri();
+ if (u == null)
+ return true;
+
+ if (preWork != null)
+ preWork.run();
+
+ Intent intent = new Intent();
+ intent.setAction(Intent.ACTION_SEND);
+ intent.setType(mGetter.getCurrentImage().getMimeType());
+ intent.putExtra(Intent.EXTRA_STREAM, u);
+ try {
+ activity.startActivity(Intent.createChooser(intent,
+ activity.getText(R.string.sendVideo)));
+ } catch (android.content.ActivityNotFoundException ex) {
+ Toast.makeText(activity, R.string.no_way_to_share_video, Toast.LENGTH_SHORT).show();
+
+ if (postWork != null)
+ postWork.run();
+ }
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_share);
+ }
+
+ if ((inclusions & INCLUDE_DELETE_MENU) != 0) {
+ MenuItem deleteMenu = menu.add(VIDEO_SAVING_ITEM, MENU_VIDEO_TOSS, 0, R.string.camera_toss).setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ if (preWork != null)
+ preWork.run();
+
+ activity.getContentResolver().delete(mGetter.getCurrentImageUri(), null, null);
+
+ if (onDelete != null)
+ onDelete.run();
+
+ if (postWork != null)
+ postWork.run();
+
+ return true;
+ }
+ });
+ deleteMenu.setIcon(android.R.drawable.ic_menu_delete);
+ deleteMenu.setAlphabeticShortcut('d');
+ }
+
+ return null;
+ }
+
+ static void deletePhoto(Activity activity, final Runnable onDelete) {
+ boolean confirm = android.preference.PreferenceManager.getDefaultSharedPreferences(activity).getBoolean("pref_gallery_confirm_delete_key", true);
+ if (!confirm) {
+ if (onDelete != null)
+ onDelete.run();
+ } else {
+ android.app.AlertDialog.Builder b = new android.app.AlertDialog.Builder(activity);
+ b.setIcon(R.drawable.delete_image);
+ b.setTitle(R.string.confirm_delete_title);
+ b.setMessage(R.string.confirm_delete_message);
+ b.setPositiveButton(android.R.string.ok, new android.content.DialogInterface.OnClickListener() {
+ public void onClick(android.content.DialogInterface v, int x) {
+ if (onDelete != null)
+ onDelete.run();
+ }
+ });
+ b.setNegativeButton(android.R.string.cancel, new android.content.DialogInterface.OnClickListener() {
+ public void onClick(android.content.DialogInterface v, int x) {
+
+ }
+ });
+ b.create().show();
+ }
+ }
+
+ static MenuItem addFlipOrientation(Menu menu, final Activity activity, final SharedPreferences prefs) {
+ // position 41 after rotate
+ return menu
+ .add(Menu.CATEGORY_SECONDARY, 304, 41, R.string.flip_orientation)
+ .setOnMenuItemClickListener(
+ new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ int current = activity.getRequestedOrientation();
+ int newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ if (current == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
+ newOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putInt("nuorientation", newOrientation);
+ editor.commit();
+ requestOrientation(activity, prefs);
+ return true;
+ }
+ })
+ .setIcon(android.R.drawable.ic_menu_always_landscape_portrait);
+ }
+
+ static void requestOrientation(Activity activity, SharedPreferences prefs) {
+ int req = prefs.getInt("nuorientation",
+ android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+ // A little trick: use USER instead of UNSPECIFIED, so we ignore the
+ // orientation set by the activity below. It may have forced a landscape
+ // orientation, which the user has now cleared here.
+ activity.setRequestedOrientation(
+ req == android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+ ? android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER
+ : req);
+ }
+
+ static public class YouTubeUploadInfoDialog extends Dialog {
+ private CheckBox mPrivate;
+ private ImageManager.VideoObject mVideo;
+ private EditText mTitle;
+ private EditText mTags;
+ private EditText mDescription;
+ private Spinner mCategory;
+ private Button mUpload;
+
+ public YouTubeUploadInfoDialog(final Activity activity,
+ final ArrayList<String> categoriesShort,
+ final ArrayList<String> categoriesLong,
+ ImageManager.VideoObject video,
+ final Runnable postRunnable) {
+ super(activity, android.R.style.Theme_Dialog);
+ mVideo = video;
+ setContentView(R.layout.youtube_upload_info);
+ setTitle(R.string.upload_dialog_title);
+
+ mPrivate = (CheckBox)findViewById(R.id.public_or_private);
+ if (!mPrivate.isChecked()) {
+ mPrivate.setChecked(true);
+ }
+
+ mTitle = (EditText)findViewById(R.id.video_title);
+ mTags = (EditText)findViewById(R.id.video_tags);
+ mDescription = (EditText)findViewById(R.id.video_description);
+ mCategory = (Spinner)findViewById(R.id.category);
+
+ if (Config.LOGV)
+ Log.v(TAG, "setting categories in adapter");
+ android.widget.ArrayAdapter<String> categories = new android.widget.ArrayAdapter<String>(activity, android.R.layout.simple_spinner_item, categoriesLong);
+ categories.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ mCategory.setAdapter(categories);
+
+ if (mVideo != null) {
+ mTitle.setText(mVideo.getTitle());
+ mTags.setText(mVideo.getTags());
+ mDescription.setText(mVideo.getDescription());
+ }
+
+ mUpload = (Button)findViewById(R.id.do_upload);
+ mUpload.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v)
+ {
+ if (mVideo != null) {
+ mVideo.setName(mTitle.getText().toString());
+ mVideo.setDescription(mDescription.getText().toString());
+ mVideo.setTags(mTags.getText().toString());
+ }
+
+ YouTubeUploadInfoDialog.this.dismiss();
+ UploadAction.uploadImage(activity, mVideo);
+ if (postRunnable != null)
+ postRunnable.run();
+ }
+ });
+ }
+ }
+}
+
diff --git a/src/com/android/camera/PickWallpaper.java b/src/com/android/camera/PickWallpaper.java
new file mode 100644
index 0000000..e1fe784
--- /dev/null
+++ b/src/com/android/camera/PickWallpaper.java
@@ -0,0 +1,23 @@
+/*
+ * 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;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the standard pick action.
+ */
+public class PickWallpaper extends Wallpaper {
+}
diff --git a/src/com/android/camera/PwaUpload.java b/src/com/android/camera/PwaUpload.java
new file mode 100644
index 0000000..8df08df
--- /dev/null
+++ b/src/com/android/camera/PwaUpload.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2006 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.Intent;
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Log;
+import android.net.Uri;
+
+/**
+ *
+ */
+public class PwaUpload extends Activity
+{
+ private static final String TAG = "camera";
+
+ @Override public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ ImageManager.IImageList imageList = ImageManager.instance().allImages(
+ this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES|ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+ Uri uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "uri is " + uri);
+ ImageManager.IImage imageObj = imageList.getImageForUri(uri);
+
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "imageObj is " + imageObj);
+ if (imageObj != null) {
+ UploadAction.uploadImage(this, imageObj);
+ }
+ finish();
+ }
+
+ @Override public void onResume() {
+ super.onResume();
+ }
+}
diff --git a/src/com/android/camera/SelectedImageGetter.java b/src/com/android/camera/SelectedImageGetter.java
new file mode 100644
index 0000000..9e8fb96
--- /dev/null
+++ b/src/com/android/camera/SelectedImageGetter.java
@@ -0,0 +1,25 @@
+/*
+ * 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 android.net.Uri;
+
+interface SelectedImageGetter {
+ ImageManager.IImage getCurrentImage();
+ Uri getCurrentImageUri();
+}
+
diff --git a/src/com/android/camera/SlideShow.java b/src/com/android/camera/SlideShow.java
new file mode 100644
index 0000000..ee6c7be
--- /dev/null
+++ b/src/com/android/camera/SlideShow.java
@@ -0,0 +1,425 @@
+/*
+ * 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 static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Paint;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+import android.widget.ImageView;
+import android.widget.ViewSwitcher;
+import android.widget.Gallery.LayoutParams;
+
+import android.view.WindowManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import com.android.camera.ImageManager.IGetBitmap_cancelable;
+import com.android.camera.ImageManager.IImage;
+import com.android.camera.ImageManager.IImageList;
+
+import android.view.MotionEvent;
+
+public class SlideShow extends Activity implements ViewSwitcher.ViewFactory
+{
+ static final private String TAG = "SlideShow";
+ static final int sLag = 2000;
+ static final int sNextImageInterval = 3000;
+ private ImageManager.IImageList mImageList;
+ private int mCurrentPosition = 0;
+ private ImageView mSwitcher;
+ private boolean mPosted = false;
+
+ @Override
+ protected void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ Window wp = getWindow();
+ wp.setFlags(FLAG_KEEP_SCREEN_ON, FLAG_KEEP_SCREEN_ON);
+
+ setContentView(R.layout.slide_show);
+
+ mSwitcher = (ImageView)findViewById(R.id.imageview);
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "mSwitcher " + mSwitcher);
+ }
+
+ @Override
+ protected void onResume()
+ {
+ super.onResume();
+
+ if (mImageList == null) {
+ mImageList = new FileImageList();
+ mCurrentPosition = 0;
+ }
+ loadImage();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ cancelPost();
+ }
+
+ static public class ImageViewTouch extends ImageView {
+ class xy {
+ public xy(float xIn, float yIn) {
+ x = xIn;
+ y = yIn;
+ timeAdded = System.currentTimeMillis();
+ }
+ public xy(MotionEvent e) {
+ x = e.getX();
+ y = e.getY();
+ timeAdded = System.currentTimeMillis();
+ }
+ float x,y;
+ long timeAdded;
+ }
+
+ SlideShow mSlideShow;
+ Paint mPaints[] = new Paint[1];
+ ArrayList<xy> mPoints = new ArrayList<xy>();
+ boolean mDown;
+
+ public ImageViewTouch(Context context) {
+ super(context);
+ mSlideShow = (SlideShow) context;
+ setScaleType(ImageView.ScaleType.CENTER);
+ setupPaint();
+ }
+
+ public ImageViewTouch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mSlideShow = (SlideShow) context;
+ setScaleType(ImageView.ScaleType.CENTER);
+ setupPaint();
+ }
+
+ private void setupPaint() {
+ for (int i = 0; i < mPaints.length; i++) {
+ Paint p = new Paint();
+ p.setARGB(255, 255, 255, 0);
+ p.setAntiAlias(true);
+ p.setStyle(Paint.Style.FILL);
+ p.setStrokeWidth(3F);
+ mPaints[i] = p;
+ }
+ }
+
+ private void addEvent(MotionEvent event) {
+ long now = System.currentTimeMillis();
+ mPoints.add(new xy(event));
+ for (int i = 0; i < event.getHistorySize(); i++)
+ mPoints.add(new xy(event.getHistoricalX(i), event.getHistoricalY(i)));
+ while (mPoints.size() > 0) {
+ xy ev = mPoints.get(0);
+ if (now - ev.timeAdded < sLag)
+ break;
+ mPoints.remove(0);
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ addEvent(event);
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mDown = true;
+ mSlideShow.cancelPost();
+ postInvalidate();
+ break;
+ case MotionEvent.ACTION_UP:
+ mDown = false;
+ postInvalidate();
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mSlideShow.cancelPost();
+ postInvalidate();
+ break;
+ }
+ return true;
+ }
+
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ boolean didPaint = false;
+ long now = System.currentTimeMillis();
+ for (xy ev: mPoints) {
+ Paint p = mPaints[0];
+ long delta = now - ev.timeAdded;
+ if (delta > sLag)
+ continue;
+
+ int alpha2 = Math.max(0, 255 - (255 * (int)delta / sLag));
+ if (alpha2 == 0)
+ continue;
+ p.setAlpha(alpha2);
+ canvas.drawCircle(ev.x, ev.y, 2, p);
+ didPaint = true;
+ }
+ if (didPaint && !mDown)
+ postInvalidate();
+ }
+ }
+
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_LEFT:
+ cancelPost();
+ loadPreviousImage();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_RIGHT:
+ cancelPost();
+ loadNextImage();
+ return true;
+
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ if (mPosted)
+ cancelPost();
+ else
+ loadNextImage();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void cancelPost() {
+ mHandler.removeCallbacks(mNextImageRunnable);
+ mPosted = false;
+ }
+
+ private void post() {
+ mHandler.postDelayed(mNextImageRunnable, sNextImageInterval);
+ mPosted = true;
+ }
+
+ private void loadImage() {
+ ImageManager.IImage image = mImageList.getImageAt(mCurrentPosition);
+ if (image == null)
+ return;
+
+ Bitmap bitmap = image.thumbBitmap();
+ if (bitmap == null)
+ return;
+
+ mSwitcher.setImageDrawable(new BitmapDrawable(bitmap));
+ post();
+ }
+
+ private Runnable mNextImageRunnable = new Runnable() {
+ public void run() {
+ if (android.util.Config.LOGV)
+ Log.v(TAG, "mNextImagerunnable called");
+ loadNextImage();
+ }
+ };
+
+ private void loadNextImage() {
+ if (++mCurrentPosition >= mImageList.getCount())
+ mCurrentPosition = 0;
+ loadImage();
+ }
+
+ private void loadPreviousImage() {
+ if (mCurrentPosition == 0)
+ mCurrentPosition = mImageList.getCount() - 1;
+ else
+ mCurrentPosition -= 1;
+
+ loadImage();
+ }
+
+ public View makeView() {
+ ImageView i = new ImageView(this);
+ i.setBackgroundColor(0xFF000000);
+ i.setScaleType(ImageView.ScaleType.FIT_CENTER);
+ i.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
+ return i;
+ }
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ };
+
+ class FileImageList implements IImageList {
+ public HashMap<String, String> getBucketIds() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void checkThumbnails(ThumbCheckCallback cb) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void commitChanges() {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void removeOnChangeListener(OnChange changeCallback) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void setOnChangeListener(OnChange changeCallback, Handler h) {
+ // TODO Auto-generated method stub
+
+ }
+
+ private ArrayList<FileImage> mImages = new ArrayList<FileImage>();
+ // image uri ==> Image object
+ private HashMap<Long, IImage> mCache = new HashMap<Long, IImage>();
+
+ class FileImage extends ImageManager.SimpleBaseImage {
+ long mId;
+ String mPath;
+
+ FileImage(long id, String path) {
+ mId = id;
+ mPath = path;
+ }
+
+ public long imageId() {
+ return mId;
+ }
+
+ public String getDataPath() {
+ return mPath;
+ }
+
+ public Bitmap fullSizeBitmap(int targetWidthOrHeight) {
+ return BitmapFactory.decodeFile(mPath);
+ }
+
+ public IGetBitmap_cancelable fullSizeBitmap_cancelable(int targetWidthOrHeight) {
+ return null;
+ }
+
+ public Bitmap thumbBitmap() {
+ Bitmap b = fullSizeBitmap(320);
+ Matrix m = new Matrix();
+ float scale = 320F / (float) b.getWidth();
+ m.setScale(scale, scale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true);
+ return scaledBitmap;
+ }
+
+ public Bitmap miniThumbBitmap() {
+ return thumbBitmap();
+ }
+
+ public long fullSizeImageId() {
+ return mId;
+ }
+ }
+
+ private void enumerate(String path, ArrayList<String> list) {
+ File f = new File(path);
+ if (f.isDirectory()) {
+ String [] children = f.list();
+ if (children != null) {
+ for (int i = 0; i < children.length; i++) {
+ if (children[i].charAt(0) != '.')
+ enumerate(path + "/" + children[i], list);
+ }
+ }
+ } else {
+ if (path.endsWith(".jpeg") || path.endsWith(".jpg") || path.endsWith(".png")) {
+ if (f.length() > 0) {
+ list.add(path);
+ }
+ }
+ }
+ }
+
+ public FileImageList() {
+ ArrayList<String> list = new ArrayList<String>();
+ enumerate(Environment.getExternalStorageDirectory().getPath(), list);
+ enumerate("/data/images", list);
+
+ for (int i = 0; i < list.size(); i++) {
+ FileImage img = new FileImage(i, list.get(i));
+ mCache.put((long)i, img);
+ mImages.add(img);
+ }
+ }
+
+ public IImage getImageAt(int i) {
+ if (i >= mImages.size())
+ return null;
+
+ return mImages.get(i);
+ }
+
+ public IImage getImageForUri(Uri uri) {
+ // TODO make this a hash lookup
+ int count = getCount();
+ for (int i = 0; i < count; i++) {
+ IImage image = getImageAt(i);
+ if (image.fullSizeImageUri().equals(uri)) {
+ return image;
+ }
+ }
+ return null;
+ }
+
+ public IImage getImageWithId(long id) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void removeImageAt(int i) {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean removeImage(IImage image) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int getCount() {
+ return mImages.size();
+ }
+
+ public void deactivate() {
+ // nothing to do here
+ }
+ }
+
+}
diff --git a/src/com/android/camera/UploadAction.java b/src/com/android/camera/UploadAction.java
new file mode 100644
index 0000000..41cc351
--- /dev/null
+++ b/src/com/android/camera/UploadAction.java
@@ -0,0 +1,34 @@
+/*
+ * 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 android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Config;
+import android.util.Log;
+
+public class UploadAction {
+ static private final String TAG = "UploadAction";
+
+ static public void uploadImage(Activity activity, ImageManager.IImage image) {
+ Bundle args = new Bundle();
+ if (image != null)
+ args.putString("imageuri", image.fullSizeImageUri().toString());
+ activity.startService(new Intent(activity, UploadService.class).putExtras(args));
+ }
+}
diff --git a/src/com/android/camera/UploadService.java b/src/com/android/camera/UploadService.java
new file mode 100644
index 0000000..9c7d2b0
--- /dev/null
+++ b/src/com/android/camera/UploadService.java
@@ -0,0 +1,1181 @@
+/*
+ * Copyright (C) 2006 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.camera.ImageManager.IImage;
+import com.android.internal.http.multipart.Part;
+import com.android.internal.http.multipart.MultipartEntity;
+import com.android.internal.http.multipart.PartBase;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.preference.PreferenceManager;
+import android.sax.Element;
+import android.sax.ElementListener;
+import android.sax.EndTextElementListener;
+import android.sax.RootElement;
+import android.util.Config;
+import android.util.Log;
+import android.util.Xml;
+
+import com.google.android.googleapps.GoogleLoginCredentialsResult;
+import com.google.android.googlelogin.GoogleLoginServiceBlockingHelper;
+import com.google.android.googlelogin.GoogleLoginServiceConstants;
+import com.google.android.googlelogin.GoogleLoginServiceNotFoundException;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+
+import android.net.http.AndroidHttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpGet;
+import com.android.internal.http.multipart.StringPart;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.params.HttpParams;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.util.EncodingUtils;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class UploadService extends Service implements Runnable {
+ private static final String TAG = "UploadService";
+
+ static final boolean DEBUG = false;
+ private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
+
+ private GoogleLoginServiceBlockingHelper mGls;
+ static public final int MSG_STATUS = 3;
+ static public final int EVENT_UPLOAD_ERROR = 400;
+
+ static public final String sPicasaService = "lh2";
+ static public final String sYouTubeService = "youtube";
+ static public final String sYouTubeUserService = "YouTubeUser";
+
+ static public final String sUploadAlbumName = "android_upload";
+ HashMap<String, Album> mAlbums;
+ ArrayList<String> mAndroidUploadAlbumPhotos = null;
+ HashMap<String, String> mGDataAuthTokenMap = new HashMap<String, String>();
+
+ int mStartId;
+ Thread mThread;
+
+ android.os.Handler mHandler = new android.os.Handler() {
+
+ };
+
+ ArrayList<Runnable> mStatusListeners = new ArrayList<Runnable>();
+
+ ArrayList<Uri> mUploadList = new ArrayList<Uri>();
+
+ ImageManager.IImageList mImageList = null;
+
+ String mPicasaUsername;
+ String mPicasaAuthToken;
+ String mYouTubeUsername;
+ String mYouTubeAuthToken;
+
+ AndroidHttpClient mClient = AndroidHttpClient.newInstance("Android-Camera/0.1");
+
+ private static final ComponentName sLogin = new ComponentName(
+ "com.google.android.googleapps",
+ "com.google.android.googleapps.GoogleLoginService");
+
+ public UploadService() {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "UploadService Constructor !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }
+
+ private void computeAuthToken() {
+ if (LOCAL_LOGV) Log.v(TAG, "computeAuthToken()");
+ if (mPicasaAuthToken != null) return;
+
+ try {
+ String account = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE);
+ GoogleLoginCredentialsResult result =
+ mGls.getCredentials(account, sPicasaService, true);
+ mPicasaAuthToken = result.getCredentialsString();
+ mPicasaUsername = result.getAccount();
+ if (Config.LOGV)
+ Log.v(TAG, "mPicasaUsername is " + mPicasaUsername);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not get auth token", e);
+ }
+ }
+
+ private void computeYouTubeAuthToken() {
+ if (LOCAL_LOGV) Log.v(TAG, "computeYouTubeAuthToken()");
+ if (mYouTubeAuthToken != null) return;
+
+ try {
+ String account = mGls.getAccount(GoogleLoginServiceConstants.REQUIRE_GOOGLE);
+ GoogleLoginCredentialsResult result =
+ mGls.getCredentials(account, sYouTubeService, true);
+ mYouTubeAuthToken = result.getCredentialsString();
+ mYouTubeUsername = result.getAccount();
+ if (mYouTubeAuthToken.equals("NoLinkedYouTubeAccount")) {
+ // we successfully logged in to the google account, but it
+ // is not linked to a YouTube username.
+ if (Config.LOGV)
+ Log.v(TAG, "account " + mYouTubeUsername + " is not linked to a youtube account");
+ mYouTubeAuthToken = null;
+ return;
+ }
+
+ mYouTubeUsername = mGls.peekCredentials(mYouTubeUsername, sYouTubeUserService);
+ // now mYouTubeUsername is the YouTube username linked to the
+ // google account, which is probably what we want to display.
+
+ if (Config.LOGV)
+ Log.v(TAG, "3 mYouTubeUsername: " + mYouTubeUsername);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not get auth token", e);
+ }
+ }
+
+ NotificationManager mNotificationManager;
+
+ @Override
+ public void onCreate() {
+
+ try {
+ mGls = new GoogleLoginServiceBlockingHelper(this);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not find google login service, stopping service");
+ stopSelf();
+ }
+
+ if (mThread == null) {
+ mThread = new Thread(this);
+ mThread.start();
+ }
+ mNotificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
+
+ IntentFilter intentFilter = new IntentFilter("com.android.camera.NEW_PICTURE");
+ b = new android.content.BroadcastReceiver() {
+ public void onReceive(android.content.Context ctx, Intent intent) {
+ android.content.SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ctx);
+ if (prefs.getBoolean("pref_camera_autoupload_key", false)) {
+ if (Config.LOGV)
+ Log.v(TAG, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> auto upload " + intent.getData());
+ }
+ }
+ };
+ registerReceiver(b, intentFilter);
+ }
+
+ android.content.BroadcastReceiver b = null;
+
+ @Override
+ public void onDestroy() {
+ mGls.close();
+ if (b != null) {
+ unregisterReceiver(b);
+ }
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "UploadService.onStart; this is " + hashCode());
+
+ if (mImageList == null) {
+ mImageList = ImageManager.instance().allImages(
+ this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_IMAGES | ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+ mImageList.setOnChangeListener(new ImageManager.IImageList.OnChange() {
+ public void onChange(ImageManager.IImageList list) {
+ /*
+ Log.v(TAG, "onChange <<<<<<<<<<<<<<<<<<<<<<<<<");
+ for (int i = 0; i < list.getCount(); i++) {
+ ImageManager.IImage img = list.getImageAt(i);
+ Log.v(TAG, "pos " + i + " " + img.fullSizeImageUri());
+ String picasaId = img.getPicasaId();
+ if (picasaId == null || picasaId.length() == 0) {
+ synchronized (mUploadList) {
+ Uri uri = img.fullSizeImageUri();
+ if (mUploadList.contains(uri)) {
+ mUploadList.add(img.fullSizeImageUri());
+ mUploadList.notify();
+ }
+ }
+ }
+ }
+ */
+ }
+ }, mHandler);
+ }
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "got image list with count " + mImageList.getCount());
+
+ synchronized (mUploadList) {
+ mStartId = startId;
+ String uriString = intent.getStringExtra("imageuri");
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "starting UploadService; startId = " + startId + " start uri: " + uriString);
+
+ if (uriString != null) {
+ Uri uri = Uri.parse(uriString);
+ IImage image = mImageList.getImageForUri(uri);
+ if (!mUploadList.contains(uri)) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "queing upload of " + image.fullSizeImageUri());
+ mUploadList.add(uri);
+ }
+ } else {
+ // for now upload all applies to images only, not videos
+ for (int i = 0; i < mImageList.getCount(); i++) {
+ IImage image = mImageList.getImageAt(i);
+ if (image instanceof ImageManager.Image) {
+ Uri uri = image.fullSizeImageUri();
+ if (!mUploadList.contains(uri)) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "queing upload of " + image.fullSizeImageUri());
+ mUploadList.add(uri);
+ }
+ }
+ }
+ }
+ updateNotification();
+ }
+
+ synchronized(mUploadList) {
+ mUploadList.notify();
+ }
+ }
+
+ void updateNotification() {
+ int videosCount = 0, imagesCount = 0;
+ for (int i = 0;i < mUploadList.size(); i++) {
+ // TODO yes this is a hack
+ Uri uri = mUploadList.get(i);
+ if (uri.toString().contains("video"))
+ videosCount += 1;
+ else
+ imagesCount += 1;
+ }
+ updateNotification(imagesCount, videosCount);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // This is the object that recieves interactions from clients.
+ private final IBinder mBinder = new Binder() {
+ protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
+ return true;
+ }
+ };
+
+ private void updateNotification(int pendingImagesCount, int pendingVideosCount) {
+ final int mVideoUploadId = 1;
+ final int mImageUploadId = 2;
+ if (pendingImagesCount == 0) {
+ if (mNotificationManager != null)
+ mNotificationManager.cancel(mImageUploadId);
+ } else {
+ String detailedMsg = String.format(getResources().getString(R.string.uploadingNPhotos), pendingImagesCount);
+ Notification n = new Notification(
+ this,
+ android.R.drawable.stat_sys_upload,
+ getResources().getString(R.string.uploading_photos),
+ System.currentTimeMillis(),
+ getResources().getString(R.string.uploading_photos_2),
+ detailedMsg,
+ null);
+ mNotificationManager.notify(mImageUploadId, n);
+ }
+ if (pendingVideosCount == 0) {
+ if (mNotificationManager != null)
+ mNotificationManager.cancel(mVideoUploadId);
+ } else {
+ String detailedMsg = String.format(getResources().getString(R.string.uploadingNVideos), pendingImagesCount);
+ Notification n = new Notification(
+ this,
+ android.R.drawable.stat_sys_upload,
+ getResources().getString(R.string.uploading_videos),
+ System.currentTimeMillis(),
+ getResources().getString(R.string.uploading_videos_2),
+ detailedMsg,
+ null);
+ mNotificationManager.notify(mVideoUploadId, n);
+ }
+ }
+
+ public void run() {
+ try {
+ if (Config.LOGV)
+ Log.v(TAG, "running upload thread...");
+ while (true) {
+ IImage image = null;
+ synchronized (mUploadList) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "mUploadList.size() is " + mUploadList.size());
+ if (mUploadList.size() == 0) {
+ try {
+ updateNotification(0, 0);
+ if (Config.LOGV)
+ Log.v(TAG, "waiting...");
+ mUploadList.wait(60000);
+ if (Config.LOGV)
+ Log.v(TAG, "done waiting...");
+ } catch (InterruptedException ex) {
+ }
+ if (mUploadList.size() == 0) {
+// if (LOCAL_LOGV) Log.v(TAG, "exiting run, stoping service");
+// stopSelf(mStartId);
+// break;
+ continue;
+ }
+ }
+ Uri uri = mUploadList.get(0);
+ image = mImageList.getImageForUri(uri);
+ if (Config.LOGV)
+ Log.v(TAG, "got uri " + uri + " " + image);
+ }
+
+ boolean success = false;
+ if (image != null) {
+ updateNotification();
+
+ long t1 = System.currentTimeMillis();
+ success = uploadItem(image);
+ long t2 = System.currentTimeMillis();
+ if (LOCAL_LOGV) Log.v(TAG, "upload took " + (t2-t1) + "; success = " + success);
+ }
+
+ synchronized (mUploadList) {
+ mUploadList.remove(0);
+ if (!success && image != null) {
+ mUploadList.add(image.fullSizeImageUri());
+ }
+ }
+ if (!success) {
+ int retryDelay = 30000;
+ if (LOCAL_LOGV)
+ Log.v(TAG, "failed to upload " + image.fullSizeImageUri() + " trying again in " + retryDelay + " ms");
+ try {
+ synchronized (mUploadList) {
+ long t1x = System.currentTimeMillis();
+ mUploadList.wait(retryDelay);
+ long t2x = System.currentTimeMillis();
+ if (Config.LOGV)
+ Log.v(TAG, "retry waited " + (t2x-t1x));
+ }
+ } catch (InterruptedException ex) {
+ if (Config.LOGV)
+ Log.v(TAG, "ping, was waiting but now retry again");
+ };
+ }
+ }
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception in upload thread", ex);
+ }
+ finally {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "finished task");
+ }
+ }
+
+ private String getLatLongString(IImage image) {
+ if (image.hasLatLong()) {
+ return "<georss:where><gml:Point><gml:pos>"
+ + image.getLatitude()
+ + " "
+ + image.getLongitude()
+ + "</gml:pos></gml:Point></georss:where>";
+ } else {
+ return "";
+ }
+ }
+
+ private String uploadAlbumName() {
+ android.content.SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ String s = prefs.getString("pref_camera_upload_albumname_key", sUploadAlbumName);
+ return s;
+ }
+
+ private boolean uploadItem(IImage image) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "starting work on " + image);
+
+ if (image instanceof ImageManager.VideoObject) {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Uploading video");
+ computeYouTubeAuthToken();
+ return (new VideoUploadTask(image)).upload();
+ } else {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "Uploading photo");
+
+ computeAuthToken();
+ // handle photos
+ if (mAlbums == null)
+ mAlbums = getAlbums();
+
+ String albumName = uploadAlbumName();
+ if (mAlbums == null || !mAlbums.containsKey(albumName)) {
+ Album a = createAlbum(albumName, uploadAlbumName());
+ if (a == null) {
+ return false;
+ }
+ if (LOCAL_LOGV)
+ Log.v(TAG, "made new album: " + a.getAlbumName() + "; " + a.getAlbumId());
+ mAlbums.put(a.getAlbumName(), a);
+ }
+
+ if (mAndroidUploadAlbumPhotos == null)
+ mAndroidUploadAlbumPhotos = getAlbumContents(albumName);
+
+ if (mAndroidUploadAlbumPhotos != null) {
+ String previousUploadId = image.getPicasaId();
+ if (previousUploadId != null) {
+ if (mAndroidUploadAlbumPhotos.contains(previousUploadId)) {
+ if (Config.LOGV)
+ Log.v(TAG, "already have id " + previousUploadId);
+ return true;
+ }
+ }
+ }
+ Album album = mAlbums.get(albumName);
+ return (new ImageUploadTask(image)).upload(album);
+ }
+ }
+
+// void broadcastError(int error) {
+// HashMap map = new HashMap();
+// map.put("error", new Integer(error));
+//
+// Message send = Message.obtain();
+// send.what = EVENT_UPLOAD_ERROR;
+// send.setData(map);
+//
+// if (mBroadcaster == null) {
+// mBroadcaster = new Broadcaster();
+// }
+// mBroadcaster.broadcast(send);
+// }
+
+ class Album {
+ String mAlbumName;
+
+ String mAlbumId;
+
+ public Album() {
+ }
+
+ public void setAlbumName(String albumName) {
+ mAlbumName = albumName;
+ }
+
+ public void setAlbumId(String albumId) {
+ mAlbumId = albumId;
+ }
+
+ public String getAlbumName() {
+ return mAlbumName;
+ }
+
+ public String getAlbumId() {
+ return mAlbumId;
+ }
+ }
+
+ static private String stringFromResponse(HttpResponse response) {
+ try {
+ HttpEntity entity = response.getEntity();
+ InputStream inputStream = entity.getContent();
+ StringWriter s = new StringWriter();
+ while (true) {
+ int c = inputStream.read();
+ if (c == -1)
+ break;
+ s.write((char)c);
+ }
+ inputStream.close();
+ String retval = s.toString();
+ if (Config.LOGV)
+ Log.v(TAG, "got resposne " + retval);
+ return retval;
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ abstract class UploadTask {
+ IImage mImageObj;
+
+ public UploadTask(IImage image) {
+ mImageObj = image;
+ }
+
+ public class UploadResponse {
+ private HttpResponse mStatus;
+ private String mBody;
+
+ public UploadResponse(HttpResponse status) {
+ mStatus = status;
+ mBody = stringFromResponse(status);
+ }
+
+ public int getStatus() {
+ return mStatus.getStatusLine().getStatusCode();
+ }
+
+ public String getResponse() {
+ return mBody;
+ }
+ }
+
+ class StreamPart extends PartBase {
+ InputStream mInputStream;
+ long mLength;
+
+ StreamPart(String name, InputStream inputStream, String contentType) {
+ super(name,
+ contentType == null ? "application/octet-stream" : contentType,
+ "ISO-8859-1",
+ "binary"
+ );
+ mInputStream = inputStream;
+ try {
+ mLength = inputStream.available();
+ } catch (IOException ex) {
+
+ }
+ }
+
+ @Override
+ protected long lengthOfData() throws IOException {
+ return mLength;
+ }
+
+ @Override
+ protected void sendData(OutputStream out) throws IOException {
+ byte [] buffer = new byte[4096];
+ while (true) {
+ int got = mInputStream.read(buffer);
+ if (got == -1)
+ break;
+ out.write(buffer, 0, got);
+ }
+ mInputStream.close();
+ }
+
+ @Override
+ protected void sendDispositionHeader(OutputStream out) throws IOException {
+ }
+
+ @Override
+ protected void sendContentTypeHeader(OutputStream out) throws IOException {
+ String contentType = getContentType();
+ if (contentType != null) {
+ out.write(CONTENT_TYPE_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(contentType));
+ String charSet = getCharSet();
+ if (charSet != null) {
+ out.write(CHARSET_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(charSet));
+ }
+ }
+ }
+ }
+
+ public class StringPartX extends StringPart {
+ public StringPartX(String name, String value, String charset) {
+ super(name, value, charset);
+ setContentType("application/atom+xml");
+ }
+
+ @Override
+ protected void sendDispositionHeader(OutputStream out) throws IOException {
+ }
+
+ @Override
+ protected void sendContentTypeHeader(OutputStream out) throws IOException {
+ String contentType = getContentType();
+ if (contentType != null) {
+ out.write(CONTENT_TYPE_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(contentType));
+ String charSet = getCharSet();
+ if (charSet != null) {
+ out.write(CHARSET_BYTES);
+ out.write(EncodingUtils.getAsciiBytes(charSet));
+ }
+ }
+ }
+ }
+
+ public class MultipartEntityX extends MultipartEntity {
+ public MultipartEntityX(Part[] parts, HttpParams params) {
+ super(parts, params);
+ }
+
+ @Override
+ public Header getContentType() {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append("multipart/related; boundary=");
+ buffer.append(EncodingUtils.getAsciiString(getMultipartBoundary()));
+ return new BasicHeader(HTTP.CONTENT_TYPE, buffer.toString());
+ }
+
+ }
+
+ protected UploadResponse doUpload(String uploadUrl,
+ String mimeType,
+ String data,
+ IImage imageObj,
+ String authToken,
+ String title,
+ String filename,
+ boolean youTubeAuthenticate) {
+ if (authToken == null)
+ return null;
+
+ FileInputStream inputStream = (FileInputStream)mImageObj.fullSizeImageData();
+ try {
+ HttpPost post = new HttpPost(uploadUrl);
+ post.addHeader(new BasicHeader("Authorization", "GoogleLogin auth=" + authToken));
+ if (youTubeAuthenticate) {
+ // TODO: remove hardwired key? - This is our official YouTube issued developer key to Android.
+ String youTubeDeveloperKey = "key=AI39si5Cr35CiD1IgDqD9Ua6N4dSbY-oibnLUPITmBN_rFW6qRz-hd8sTqNzRf1gzNwSYZbDuS31Txa4iKyjAV77507O4tq7JA";
+ post.addHeader("X-GData-Key", youTubeDeveloperKey);
+ post.addHeader("Slug", filename);
+ }
+
+ Part p1 = new StringPartX("param_name", data, null);
+ Part p2 = new StreamPart("field_uploadfile", inputStream, mimeType);
+
+ MultipartEntity mpe = new MultipartEntityX(new Part[] { p1, p2 }, post.getParams());
+ post.setEntity(mpe);
+ HttpResponse status = mClient.execute(post);
+ if (LOCAL_LOGV) Log.v(TAG, "doUpload response is " + status.getStatusLine());
+ return new UploadResponse(status);
+ } catch (java.io.IOException ex) {
+ if (LOCAL_LOGV) Log.v(TAG, "IOException in doUpload", ex);
+ return null;
+ }
+ }
+
+ class ResponseHandler implements ElementListener {
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASSA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+
+ private ContentHandler mHandler = null;
+ private String mId = null;
+
+ public ResponseHandler() {
+ RootElement root = new RootElement(ATOM_NAMESPACE, "entry");
+ Element entry = root;
+ entry.setElementListener(this);
+
+ entry.getChild(PICASSA_NAMESPACE, "id")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mId = body;
+ }
+ });
+
+ mHandler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ }
+
+ public void end() {
+ }
+
+ ContentHandler getContentHandler() {
+ return mHandler;
+ }
+
+ public String getId() {
+ return mId;
+ }
+ }
+ }
+
+ private class VideoUploadTask extends UploadTask {
+ public VideoUploadTask(IImage image) {
+ super(image);
+ }
+ protected String getYouTubeBaseUrl() {
+ return "http://gdata.youtube.com";
+ }
+
+ public boolean upload() {
+ String uploadUrl = "http://uploads.gdata.youtube.com"
+ + "/feeds/users/"
+ + mYouTubeUsername
+ + "/uploads?client=ytapi-google-android";
+
+ String title = mImageObj.getTitle();
+ String isPrivate = "";
+ String keywords = "";
+ String category = "";
+ if (mImageObj instanceof ImageManager.VideoObject) {
+ ImageManager.VideoObject video = (ImageManager.VideoObject)mImageObj;
+ if (mImageObj.getIsPrivate()) {
+ isPrivate = "<yt:private/>";
+ }
+ keywords = video.getTags();
+ if (keywords == null || keywords.trim().length() == 0) {
+ // there must be a keyword or YouTube will reject the video
+ keywords = getResources().getString(R.string.upload_default_tags_text);
+ }
+ // TODO: use the real category when we have the category spinner in details
+// category = video.getCategory();
+ category = "";
+ if (category == null || category.trim().length() == 0) {
+ // there must be a description or YouTube will get an internal error and return 500
+ category = getResources().getString(R.string.upload_default_category_text);
+ }
+ }
+ String description = mImageObj.getDescription();
+ if (description == null || description.trim().length() == 0) {
+ // there must be a description or YouTube will get an internal error and return 500
+ description = getResources().getString(R.string.upload_default_description_text);
+ }
+ String data = "<?xml version='1.0'?>\n"
+ + "<entry xmlns='http://www.w3.org/2005/Atom'\n"
+ + " xmlns:media='http://search.yahoo.com/mrss/'\n"
+ + " xmlns:yt='http://gdata.youtube.com/schemas/2007'>\n"
+ + " <media:group>\n"
+ + " <media:title type='plain'>" + title + "</media:title>\n" // TODO: need user entered title
+ + " <media:description type='plain'>" + description + "</media:description>\n"
+ + isPrivate
+ + " <media:category scheme='http://gdata.youtube.com/schemas/2007/categories.cat'>\n"
+ + category
+ + " </media:category>\n"
+ + " <media:keywords>" + keywords + "</media:keywords>\n"
+ + " </media:group>\n"
+ + "</entry>";
+
+ if (LOCAL_LOGV) Log.v("youtube", "uploadUrl: " + uploadUrl);
+ if (LOCAL_LOGV) Log.v("youtube", "GData: " + data);
+
+ UploadResponse result = doUpload(uploadUrl,
+ "video/3gpp2",
+ data,
+ null,
+ mYouTubeAuthToken,
+ title,
+ mImageObj.fullSizeImageUri().getLastPathSegment(),
+ true);
+
+ boolean success = false;
+ if (result != null) {
+ switch (result.getStatus()) {
+ case 401:
+ if (result.getResponse().contains("Token expired")) {
+ // When we tried to upload a video to YouTube, the youtube server told us
+ // our auth token was expired. Get a new one and try again.
+ try {
+ mGls.invalidateAuthToken(mYouTubeAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate youtube auth token", e);
+ }
+ mYouTubeAuthToken = null; // Forces computeYouTubeAuthToken to get a new token.
+ computeYouTubeAuthToken();
+ }
+ break;
+
+ case 200:
+ case 201:
+ case 202:
+ case 203:
+ case 204:
+ case 205:
+ case 206:
+ success = true;
+ break;
+
+ }
+ }
+ return success;
+ }
+ }
+
+ private class ImageUploadTask extends UploadTask {
+ public ImageUploadTask(IImage image) {
+ super(image);
+ }
+
+ public boolean upload(Album album) {
+ String uploadUrl = getServiceBaseUrl()
+ + mPicasaUsername
+ + "/album/"
+ + album.getAlbumId();
+
+ String name = mImageObj.getTitle();
+ String description = mImageObj.getDescription();
+ String data = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:georss='http://www.georss.org/georss' xmlns:gml='http://www.opengis.net/gml'><title>"
+ + name
+ + "</title>"
+ + "<summary>"
+ + (description != null ? description : "")
+ + "</summary>"
+ + getLatLongString(mImageObj)
+ + "<category scheme='http://schemas.google.com/g/2005#kind' term='http://schemas.google.com/photos/2007#photo'/></entry>\n";
+
+ if (LOCAL_LOGV)
+ Log.v(TAG, "xml for image is " + data);
+ UploadResponse response = doUpload(uploadUrl,
+ "image/jpeg",
+ data,
+ mImageObj,
+ mPicasaAuthToken,
+ name,
+ name,
+ false);
+
+ if (response != null) {
+ int status = response.getStatus();
+ if (status == HttpStatus.SC_UNAUTHORIZED ||
+ status == HttpStatus.SC_FORBIDDEN ||
+ status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
+ try {
+ mGls.invalidateAuthToken(mPicasaAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate picasa auth token", e);
+ }
+ mPicasaAuthToken = null;
+ } else {
+ ResponseHandler h = new ResponseHandler();
+ try {
+ Xml.parse(response.getResponse(), h.getContentHandler());
+ String id = h.getId();
+ if (id != null && mImageObj != null) {
+ mImageObj.setPicasaId(id);
+ mAndroidUploadAlbumPhotos.add(id);
+ return true;
+ }
+ } catch (org.xml.sax.SAXException ex) {
+ Log.e(TAG, "SAXException in doUpload " + ex.toString());
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ private Album createAlbum(String name, String summary) {
+ String authToken = mPicasaAuthToken;
+ if (authToken == null)
+ return null;
+
+ try {
+ String url = getServiceBaseUrl() + mPicasaUsername;
+ HttpPost post = new HttpPost(url);
+ String entryString = "<entry xmlns='http://www.w3.org/2005/Atom' xmlns:media='http://search.yahoo.com/mrss/' xmlns:gphoto='http://schemas.google.com/photos/2007'>"
+ + "<title type='text'>"
+ + name
+ + "</title>"
+ + "<summary>"
+ + summary
+ + "</summary>"
+ + "<gphoto:access>private</gphoto:access>"
+ + "<gphoto:commentingEnabled>true</gphoto:commentingEnabled>"
+ + "<gphoto:timestamp>"
+ + String.valueOf(System.currentTimeMillis())
+ + "</gphoto:timestamp>"
+ + "<category scheme=\"http://schemas.google.com/g/2005#kind\" term=\"http://schemas.google.com/photos/2007#album\"/>"
+ + "</entry>\n";
+
+ StringEntity entity = new StringEntity(entryString);
+ entity.setContentType(new BasicHeader("Content-Type", "application/atom+xml"));
+ post.setEntity(entity);
+ post.addHeader(new BasicHeader("Authorization", "GoogleLogin auth=" + authToken));
+ HttpResponse status = mClient.execute(post);
+ if (LOCAL_LOGV)
+ Log.v(TAG, "status is " + status.getStatusLine());
+ if (status.getStatusLine().getStatusCode() < 200 || status.getStatusLine().getStatusCode() >= 300) {
+ return null;
+ }
+ Album album = new Album();
+ Xml.parse(stringFromResponse(status), new PicasaAlbumHandler(album).getContentHandler());
+ return album;
+ } catch (java.io.UnsupportedEncodingException ex) {
+ Log.e(TAG, "gak, UnsupportedEncodingException " + ex.toString());
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "IOException " + ex.toString());
+ } catch (org.xml.sax.SAXException ex) {
+ Log.e(TAG, "XmlPullParserException " + ex.toString());
+ }
+ return null;
+ }
+
+ public static String streamToString(InputStream stream, int maxChars, boolean reset)
+ throws IOException {
+ BufferedReader reader = new BufferedReader(new InputStreamReader(stream), 8192);
+ StringBuilder sb = new StringBuilder();
+ String line = null;
+
+ while ((line = reader.readLine()) != null
+ && (maxChars == -1 || sb.length() < maxChars)) {
+ sb.append(line);
+ }
+ reader.close();
+ if (reset) stream.reset();
+ return sb.toString();
+ }
+
+ InputStream get(String url) {
+ try {
+ if (LOCAL_LOGV) Log.v(TAG, "url is " + url);
+
+ for (int i = 0; i < 2; ++i) {
+ HttpGet get = new HttpGet(url);
+ get.setHeader(new BasicHeader("Authorization",
+ "GoogleLogin auth=" + mPicasaAuthToken));
+
+ HttpResponse response = mClient.execute(get);
+ if (LOCAL_LOGV) Log.v(TAG, "response is " + response.getStatusLine());
+ switch (response.getStatusLine().getStatusCode()) {
+ case HttpStatus.SC_UNAUTHORIZED:
+ case HttpStatus.SC_FORBIDDEN:
+ case HttpStatus.SC_INTERNAL_SERVER_ERROR: // http://b/1151576
+ try {
+ mGls.invalidateAuthToken(mPicasaAuthToken);
+ } catch (GoogleLoginServiceNotFoundException e) {
+ Log.e(TAG, "Could not invalidate picasa auth token", e);
+ }
+ mPicasaAuthToken = null;
+ computeAuthToken();
+ if (mPicasaAuthToken != null) {
+ // retry fetch after getting new token
+ continue;
+ }
+ break;
+ }
+
+ InputStream inputStream = response.getEntity().getContent();
+ return inputStream;
+ }
+ return null;
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "IOException");
+ }
+ return null;
+ }
+
+ private HashMap<String, Album> getAlbums() {
+ if (LOCAL_LOGV)
+ Log.v(TAG, "getAlbums");
+
+ PicasaAlbumHandler h = new PicasaAlbumHandler();
+ try {
+ String url = getServiceBaseUrl() + mPicasaUsername + "?kind=album";
+ InputStream inputStream = get(url);
+ if (inputStream == null) {
+ if (Config.LOGV)
+ Log.v(TAG, "can't get " + url + "; bail from getAlbums()");
+ mPicasaAuthToken = null;
+ return null;
+ }
+
+ Xml.parse(inputStream, Xml.findEncodingByName("UTF-8"), h.getContentHandler());
+ if (LOCAL_LOGV)
+ Log.v(TAG, "done getting albums");
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(TAG, "got exception " + e.toString());
+ e.printStackTrace();
+ } catch (SAXException e) {
+ Log.e(TAG, "got exception " + e.toString());
+ e.printStackTrace();
+ }
+ if (LOCAL_LOGV) {
+ java.util.Iterator it = h.getAlbums().keySet().iterator();
+ while (it.hasNext()) {
+ if (Config.LOGV)
+ Log.v(TAG, "album: " + (String) it.next());
+ }
+ }
+ return h.getAlbums();
+ }
+
+ ArrayList<String> getAlbumContents(String albumName) {
+ String url = getServiceBaseUrl() + mPicasaUsername + "/album/" + albumName + "?kind=photo&max-results=10000";
+ try {
+ InputStream inputStream = get(url);
+ if (inputStream == null)
+ return null;
+
+ AlbumContentsHandler ah = new AlbumContentsHandler();
+ Xml.parse(inputStream, Xml.findEncodingByName("UTF-8"), ah.getContentHandler());
+ ArrayList<String> photos = ah.getPhotos();
+ inputStream.close();
+ return photos;
+ } catch (IOException e) {
+ Log.e(TAG, "got IOException " + e.toString());
+ e.printStackTrace();
+ } catch (SAXException e) {
+ Log.e(TAG, "got SAXException " + e.toString());
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ class AlbumContentsHandler implements ElementListener {
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+
+ private ContentHandler mHandler = null;
+ private ArrayList<String> mPhotos = new ArrayList<String>();
+
+ public AlbumContentsHandler() {
+ RootElement root = new RootElement(ATOM_NAMESPACE, "feed");
+ Element entry = root.getChild(ATOM_NAMESPACE, "entry");
+
+ entry.setElementListener(this);
+
+ entry.getChild(PICASA_NAMESPACE, "id")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mPhotos.add(body);
+ }
+ });
+
+ mHandler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ }
+
+ public void end() {
+ }
+
+ ContentHandler getContentHandler() {
+ return mHandler;
+ }
+
+ public ArrayList<String> getPhotos() {
+ return mPhotos;
+ }
+ }
+
+ private String getServiceBaseUrl() {
+ return "http://picasaweb.google.com/data/feed/api/user/";
+ }
+
+
+ class PicasaAlbumHandler implements ElementListener {
+ private Album mAlbum;
+ private HashMap<String, Album> mAlbums = new HashMap<String, Album>();
+ private boolean mJustOne;
+ private static final String ATOM_NAMESPACE
+ = "http://www.w3.org/2005/Atom";
+ private static final String PICASSA_NAMESPACE
+ = "http://schemas.google.com/photos/2007";
+ private ContentHandler handler = null;
+
+ public PicasaAlbumHandler() {
+ mJustOne = false;
+ init();
+ }
+
+ public HashMap<String, Album> getAlbums() {
+ return mAlbums;
+ }
+
+ public PicasaAlbumHandler(Album album) {
+ mJustOne = true;
+ mAlbum = album;
+ init();
+ }
+
+ private void init() {
+ Element entry;
+ RootElement root;
+ if (mJustOne) {
+ root = new RootElement(ATOM_NAMESPACE, "entry");
+ entry = root;
+ } else {
+ root = new RootElement(ATOM_NAMESPACE, "feed");
+ entry = root.getChild(ATOM_NAMESPACE, "entry");
+ }
+ entry.setElementListener(this);
+
+ entry.getChild(ATOM_NAMESPACE, "title")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mAlbum.setAlbumName(body);
+ }
+ });
+
+ entry.getChild(PICASSA_NAMESPACE, "name")
+ .setEndTextElementListener(new EndTextElementListener() {
+ public void end(String body) {
+ mAlbum.setAlbumId(body);
+ }
+ });
+
+ this.handler = root.getContentHandler();
+ }
+
+ public void start(Attributes attributes) {
+ if (!mJustOne) {
+ mAlbum = new Album();
+ }
+ }
+
+ public void end() {
+ if (!mJustOne) {
+ mAlbums.put(mAlbum.getAlbumName(), mAlbum);
+ mAlbum = null;
+ }
+ }
+
+ ContentHandler getContentHandler() {
+ return handler;
+ }
+ }
+}
diff --git a/src/com/android/camera/ViewImage.java b/src/com/android/camera/ViewImage.java
new file mode 100644
index 0000000..4b9eb58
--- /dev/null
+++ b/src/com/android/camera/ViewImage.java
@@ -0,0 +1,1434 @@
+/*
+ * Copyright (C) 5163 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.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.provider.MediaStore;
+import android.util.AttributeSet;
+import android.util.Config;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.Animation;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.AnimationUtils;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.Scroller;
+import android.widget.TextView;
+import android.widget.ZoomControls;
+import android.preference.PreferenceManager;
+
+import com.android.camera.ImageManager.IImage;
+
+import java.util.Random;
+
+public class ViewImage extends Activity
+{
+ static final String TAG = "ViewImage";
+ private ImageGetter mGetter;
+
+ static final boolean sSlideShowHidesStatusBar = true;
+
+ // Choices for what adjacents to load.
+ static private final int[] sOrder_adjacents = new int[] { 0, 1, -1 };
+ static private final int[] sOrder_slideshow = new int[] { 0 };
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ }
+ };
+
+ private Random mRandom = new Random(System.currentTimeMillis());
+ private int [] mShuffleOrder;
+ private boolean mUseShuffleOrder = false;
+ private boolean mSlideShowLoop = false;
+
+ private static final int MODE_NORMAL = 1;
+ private static final int MODE_SLIDESHOW = 2;
+ private int mMode = MODE_NORMAL;
+
+ private boolean mSortAscending = false;
+ private int mSlideShowInterval;
+ private int mLastSlideShowImage;
+ private boolean mFirst = true;
+ private int mCurrentPosition = 0;
+ private boolean mLayoutComplete = false;
+
+ // represents which style animation to use
+ private int mAnimationIndex;
+ private Animation [] mSlideShowInAnimation;
+ private Animation [] mSlideShowOutAnimation;
+
+ private SharedPreferences mPrefs;
+ private MenuItem mFlipItem;
+
+ private View mNextImageView, mPrevImageView;
+ private Animation mHideNextImageViewAnimation = new AlphaAnimation(1F, 0F);
+ private Animation mHidePrevImageViewAnimation = new AlphaAnimation(1F, 0F);
+ private Animation mShowNextImageViewAnimation = new AlphaAnimation(0F, 1F);
+ private Animation mShowPrevImageViewAnimation = new AlphaAnimation(0F, 1F);
+
+
+ static final int sPadding = 20;
+ static final int sHysteresis = sPadding * 2;
+ static final int sBaseScrollDuration = 1000; // ms
+
+ private ImageManager.IImageList mAllImages;
+
+ private int mSlideShowImageCurrent = 0;
+ private ImageViewTouch [] mSlideShowImageViews = new ImageViewTouch[2];
+
+
+ // Array of image views. The center view is the one the user is focused
+ // on. The one at the zeroth position and the second position reflect
+ // the images to the left/right of the center image.
+ private ImageViewTouch[] mImageViews = new ImageViewTouch[3];
+
+ // Container for the three image views. This guy can be "scrolled"
+ // to reveal the image prior to and after the center image.
+ private ScrollHandler mScroller;
+
+ private MenuHelper.MenuItemsResult mImageMenuRunnable;
+
+ Runnable mDismissOnScreenControlsRunnable;
+ ZoomControls mZoomControls;
+
+ public ViewImage() {
+ }
+
+ private void updateNextPrevControls() {
+ boolean showPrev = mCurrentPosition > 0;
+ boolean showNext = mCurrentPosition < mAllImages.getCount() - 1;
+
+ boolean prevIsVisible = mPrevImageView.getVisibility() == View.VISIBLE;
+ boolean nextIsVisible = mNextImageView.getVisibility() == View.VISIBLE;
+
+ if (showPrev && !prevIsVisible) {
+ Animation a = mShowPrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.VISIBLE);
+ } else if (!showPrev && prevIsVisible) {
+ Animation a = mHidePrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.GONE);
+ }
+
+ if (showNext && !nextIsVisible) {
+ Animation a = mShowNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.VISIBLE);
+ } else if (!showNext && nextIsVisible) {
+ Animation a = mHideNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.GONE);
+ }
+ }
+
+ private void showOnScreenControls() {
+ if (mZoomControls != null) {
+ if (mZoomControls.getVisibility() == View.GONE) {
+ mZoomControls.show();
+ mZoomControls.requestFocus(); // this shouldn't be necessary
+ }
+ updateNextPrevControls();
+ scheduleDismissOnScreenControls();
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ boolean sup = super.dispatchTouchEvent(m);
+ if (sup == false) {
+ if (mMode == MODE_SLIDESHOW) {
+ mSlideShowImageViews[mSlideShowImageCurrent].handleTouchEvent(m);
+ } else if (mMode == MODE_NORMAL){
+ mImageViews[1].handleTouchEvent(m);
+ }
+ return true;
+ }
+ return true;
+ }
+
+ private void scheduleDismissOnScreenControls() {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mHandler.postDelayed(mDismissOnScreenControlsRunnable, 1500);
+ }
+
+ public View getZoomControls() {
+ if (mZoomControls == null) {
+ mZoomControls = new ZoomControls(this);
+ mZoomControls.setVisibility(View.GONE);
+ mZoomControls.setZoomSpeed(0);
+ mDismissOnScreenControlsRunnable = new Runnable() {
+ public void run() {
+ mZoomControls.hide();
+
+ if (mNextImageView.getVisibility() == View.VISIBLE) {
+ Animation a = mHideNextImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mNextImageView.setAnimation(a);
+ mNextImageView.setVisibility(View.INVISIBLE);
+ }
+
+ if (mPrevImageView.getVisibility() == View.VISIBLE) {
+ Animation a = mHidePrevImageViewAnimation;
+ a.setDuration(500);
+ a.startNow();
+ mPrevImageView.setAnimation(a);
+ mPrevImageView.setVisibility(View.INVISIBLE);
+ }
+ }
+ };
+ mZoomControls.setOnZoomInClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mImageViews[1].zoomIn();
+ scheduleDismissOnScreenControls();
+ }
+ });
+ mZoomControls.setOnZoomOutClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ mHandler.removeCallbacks(mDismissOnScreenControlsRunnable);
+ mImageViews[1].zoomOut();
+ scheduleDismissOnScreenControls();
+ }
+ });
+ }
+ return mZoomControls;
+ }
+
+ private boolean isPickIntent() {
+ String action = getIntent().getAction();
+ return (Intent.ACTION_PICK.equals(action) || Intent.ACTION_GET_CONTENT.equals(action));
+ }
+
+ private static final boolean sDragLeftRight = false;
+ private static final boolean sUseBounce = false;
+ private static final boolean sAnimateTransitions = false;
+
+ static public class ImageViewTouch extends ImageViewTouchBase {
+ private ViewImage mViewImage;
+
+ private static int TOUCH_STATE_REST = 0;
+ private static int TOUCH_STATE_LEFT_PRESS = 1;
+ private static int TOUCH_STATE_RIGHT_PRESS = 2;
+ private static int TOUCH_STATE_PANNING = 3;
+
+ private static int TOUCH_AREA_WIDTH = 60;
+
+ private int mTouchState = TOUCH_STATE_REST;
+
+ public ImageViewTouch(Context context) {
+ super(context);
+ mViewImage = (ViewImage) context;
+ }
+
+ public ImageViewTouch(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mViewImage = (ViewImage) context;
+ }
+
+ protected void postTranslate(float dx, float dy, boolean bounceOK) {
+ super.postTranslate(dx, dy);
+ if (dx != 0F || dy != 0F)
+ mViewImage.showOnScreenControls();
+
+ if (!sUseBounce) {
+ center(true, false, false);
+ }
+ }
+
+ protected ScrollHandler scrollHandler() {
+ return mViewImage.mScroller;
+ }
+
+ public boolean handleTouchEvent(MotionEvent m) {
+ int viewWidth = getWidth();
+ ViewImage viewImage = mViewImage;
+ int x = (int) m.getX();
+ int y = (int) m.getY();
+
+ switch (m.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ viewImage.setMode(MODE_NORMAL);
+ viewImage.showOnScreenControls();
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ case MotionEvent.ACTION_MOVE:
+ if (x < TOUCH_AREA_WIDTH) {
+ if (mTouchState == TOUCH_STATE_REST) {
+ mTouchState = TOUCH_STATE_LEFT_PRESS;
+ }
+ if (mTouchState == TOUCH_STATE_LEFT_PRESS) {
+ viewImage.mPrevImageView.setPressed(true);
+ viewImage.mNextImageView.setPressed(false);
+ }
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ } else if (x > viewWidth - TOUCH_AREA_WIDTH) {
+ if (mTouchState == TOUCH_STATE_REST) {
+ mTouchState = TOUCH_STATE_RIGHT_PRESS;
+ }
+ if (mTouchState == TOUCH_STATE_RIGHT_PRESS) {
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(true);
+ }
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+ } else {
+ mTouchState = TOUCH_STATE_PANNING;
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+
+ int deltaX;
+ int deltaY;
+
+ if (mLastXTouchPos == -1) {
+ deltaX = 0;
+ deltaY = 0;
+ } else {
+ deltaX = x - mLastXTouchPos;
+ deltaY = y - mLastYTouchPos;
+ }
+
+ mLastXTouchPos = x;
+ mLastYTouchPos = y;
+
+ if (mBitmapDisplayed == null)
+ return true;
+
+ if (deltaX != 0) {
+ // Second. Pan to whatever degree is possible.
+ if (getScale() > 1F) {
+ postTranslate(deltaX, deltaY, sUseBounce);
+ ImageViewTouch.this.center(true, true, false);
+ }
+ }
+ setImageMatrix(getImageViewMatrix());
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ int nextImagePos = -1;
+ if (mTouchState == TOUCH_STATE_LEFT_PRESS && x < TOUCH_AREA_WIDTH) {
+ nextImagePos = viewImage.mCurrentPosition - 1;
+ } else if (mTouchState == TOUCH_STATE_RIGHT_PRESS &&
+ x > viewWidth - TOUCH_AREA_WIDTH) {
+ nextImagePos = viewImage.mCurrentPosition + 1;
+ }
+ if (nextImagePos >= 0
+ && nextImagePos < viewImage.mAllImages.getCount()) {
+ synchronized (viewImage) {
+ viewImage.setMode(MODE_NORMAL);
+ viewImage.setImage(nextImagePos);
+ }
+ }
+ viewImage.scheduleDismissOnScreenControls();
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ case MotionEvent.ACTION_CANCEL:
+ viewImage.mPrevImageView.setPressed(false);
+ viewImage.mNextImageView.setPressed(false);
+ mTouchState = TOUCH_STATE_REST;
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event)
+ {
+ int current = mViewImage.mCurrentPosition;
+
+ int nextImagePos = -2; // default no next image
+ try {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_DPAD_CENTER: {
+ if (mViewImage.isPickIntent()) {
+ ImageManager.IImage img = mViewImage.mAllImages.getImageAt(mViewImage.mCurrentPosition);
+ mViewImage.setResult(RESULT_OK,
+ new Intent().setData(img.fullSizeImageUri()));
+ mViewImage.finish();
+ }
+ break;
+ }
+ case KeyEvent.KEYCODE_DPAD_LEFT: {
+ panBy(sPanRate, 0);
+ int maxOffset = (current == 0) ? 0 : sHysteresis;
+ if (getScale() <= 1F || isShiftedToNextImage(true, maxOffset)) {
+ nextImagePos = current - 1;
+ } else {
+ center(true, false, true);
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_RIGHT: {
+ panBy(-sPanRate, 0);
+ int maxOffset = (current == mViewImage.mAllImages.getCount()-1) ? 0 : sHysteresis;
+ if (getScale() <= 1F || isShiftedToNextImage(false, maxOffset)) {
+ nextImagePos = current + 1;
+ } else {
+ center(true, false, true);
+ }
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_UP: {
+ panBy(0, sPanRate);
+ center(true, false, false);
+ return true;
+ }
+ case KeyEvent.KEYCODE_DPAD_DOWN: {
+ panBy(0, -sPanRate);
+ center(true, false, false);
+ return true;
+ }
+ case KeyEvent.KEYCODE_DEL:
+ MenuHelper.deletePhoto(mViewImage, mViewImage.mDeletePhotoRunnable);
+ break;
+ }
+ } finally {
+ if (nextImagePos >= 0 && nextImagePos < mViewImage.mAllImages.getCount()) {
+ synchronized (mViewImage) {
+ mViewImage.setMode(MODE_NORMAL);
+ mViewImage.setImage(nextImagePos);
+ }
+ } else if (nextImagePos != -2) {
+ center(true, true, false);
+ }
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ protected boolean isShiftedToNextImage(boolean left, int maxOffset) {
+ boolean retval;
+ Bitmap bitmap = mBitmapDisplayed;
+ Matrix m = getImageViewMatrix();
+ if (left) {
+ float [] t1 = new float[] { 0, 0 };
+ m.mapPoints(t1);
+ retval = t1[0] > maxOffset;
+ } else {
+ int width = bitmap != null ? bitmap.getWidth() : getWidth();
+ float [] t1 = new float[] { width, 0 };
+ m.mapPoints(t1);
+ retval = t1[0] + maxOffset < getWidth();
+ }
+ return retval;
+ }
+
+ protected void scrollX(int deltaX) {
+ scrollHandler().scrollBy(deltaX, 0);
+ }
+
+ protected int getScrollOffset() {
+ return scrollHandler().getScrollX();
+ }
+
+ }
+
+ static class ScrollHandler extends LinearLayout {
+ private Runnable mFirstLayoutCompletedCallback = null;
+ private Scroller mScrollerHelper;
+ private int mWidth = -1;
+
+ public ScrollHandler(Context context) {
+ super(context);
+ mScrollerHelper = new Scroller(context);
+ }
+
+ public ScrollHandler(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mScrollerHelper = new Scroller(context);
+ }
+
+ public void setLayoutCompletedCallback(Runnable r) {
+ mFirstLayoutCompletedCallback = r;
+ }
+
+ public void startScrollTo(int newX, int newY) {
+ int oldX = getScrollX();
+ int oldY = getScrollY();
+
+ int deltaX = newX - oldX;
+ int deltaY = newY - oldY;
+
+ if (mWidth == -1) {
+ mWidth = findViewById(R.id.image2).getWidth();
+ }
+ int viewWidth = mWidth;
+
+ int duration = viewWidth > 0
+ ? sBaseScrollDuration * Math.abs(deltaX) / viewWidth
+ : 0;
+ mScrollerHelper.startScroll(oldX, oldY, deltaX, deltaY, duration);
+ invalidate();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (mScrollerHelper.computeScrollOffset()) {
+ scrollTo(mScrollerHelper.getCurrX(), mScrollerHelper.getCurrY());
+ postInvalidate(); // So we draw again
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ int width = right - left;
+ int x = 0;
+ for (View v : new View[] {
+ findViewById(R.id.image1),
+ findViewById(R.id.image2),
+ findViewById(R.id.image3) }) {
+ v.layout(x, 0, x + width, bottom);
+ x += (width + sPadding);
+ }
+
+ findViewById(R.id.padding1).layout(width, 0, width + sPadding, bottom);
+ findViewById(R.id.padding2).layout(width+sPadding+width, 0, width+sPadding+width+sPadding, bottom);
+
+ if (changed) {
+ if (mFirstLayoutCompletedCallback != null) {
+ mFirstLayoutCompletedCallback.run();
+ }
+ }
+ }
+ }
+
+ private void animateScrollTo(int xNew, int yNew) {
+ mScroller.startScrollTo(xNew, yNew);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ super.onCreateOptionsMenu(menu);
+
+ if (true) {
+ MenuItem item = menu.add(Menu.CATEGORY_SECONDARY, 203, 0, R.string.slide_show);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ setMode(MODE_SLIDESHOW);
+ mLastSlideShowImage = mCurrentPosition;
+ loadNextImage(mCurrentPosition, 0, true);
+ return true;
+ }
+ });
+ item.setIcon(android.R.drawable.ic_menu_slideshow);
+ }
+
+ mFlipItem = MenuHelper.addFlipOrientation(menu, ViewImage.this, mPrefs);
+ mFlipItem.setIcon(android.R.drawable.ic_menu_always_landscape_portrait);
+
+ final SelectedImageGetter selectedImageGetter = new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return mAllImages.getImageAt(mCurrentPosition);
+ }
+
+ public Uri getCurrentImageUri() {
+ return mAllImages.getImageAt(mCurrentPosition).fullSizeImageUri();
+ }
+ };
+
+ mImageMenuRunnable = MenuHelper.addImageMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL,
+ ViewImage.this,
+ mHandler,
+ mDeletePhotoRunnable,
+ new MenuHelper.MenuInvoker() {
+ public void run(MenuHelper.MenuCallback cb) {
+ setMode(MODE_NORMAL);
+ cb.run(selectedImageGetter.getCurrentImageUri(), selectedImageGetter.getCurrentImage());
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+ setImage(mCurrentPosition);
+ }
+ });
+
+ if (true) {
+ MenuItem item = menu.add(Menu.CATEGORY_SECONDARY, 203, 1000, R.string.camerasettings);
+ item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ Intent preferences = new Intent();
+ preferences.setClass(ViewImage.this, GallerySettings.class);
+ startActivity(preferences);
+ return true;
+ }
+ });
+ item.setAlphabeticShortcut('p');
+ item.setIcon(android.R.drawable.ic_menu_preferences);
+ }
+
+ // Hidden menu just so the shortcut will bring up the zoom controls
+ menu.add(Menu.CATEGORY_SECONDARY, 203, 0, R.string.camerasettings) // the string resource is a placeholder
+ .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ showOnScreenControls();
+ return true;
+ }
+ })
+ .setAlphabeticShortcut('z')
+ .setVisible(false);
+
+
+ return true;
+ }
+
+ protected Runnable mDeletePhotoRunnable = new Runnable() {
+ public void run() {
+ mAllImages.removeImageAt(mCurrentPosition);
+ if (mAllImages.getCount() == 0) {
+ finish();
+ } else {
+ if (mCurrentPosition == mAllImages.getCount()) {
+ mCurrentPosition -= 1;
+ }
+ }
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmapResetBase(null, true, true);
+ }
+ setImage(mCurrentPosition);
+ }
+ };
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu)
+ {
+ super.onPrepareOptionsMenu(menu);
+ setMode(MODE_NORMAL);
+
+ if (mImageMenuRunnable != null) {
+ mImageMenuRunnable.gettingReadyToOpen(menu, mAllImages.getImageAt(mCurrentPosition));
+ }
+
+ int keyboard = getResources().getConfiguration().keyboardHidden;
+ mFlipItem.setEnabled(keyboard == android.content.res.Configuration.KEYBOARDHIDDEN_YES);
+
+ menu.findItem(MenuHelper.MENU_IMAGE_SHARE).setEnabled(isCurrentImageShareable());
+
+ return true;
+ }
+
+ private boolean isCurrentImageShareable() {
+ IImage image = mAllImages.getImageAt(mCurrentPosition);
+ if (image != null){
+ Uri uri = image.fullSizeImageUri();
+ String fullUri = uri.toString();
+ return fullUri.startsWith(MediaStore.Images.Media.INTERNAL_CONTENT_URI.toString()) ||
+ fullUri.startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString());
+ }
+ return true;
+ }
+
+ @Override
+ public void onConfigurationChanged(android.content.res.Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmapResetBase(null, false, true);
+ }
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+
+ @Override
+ public boolean onMenuItemSelected(int featureId, MenuItem item) {
+ boolean b = super.onMenuItemSelected(featureId, item);
+ if (mImageMenuRunnable != null)
+ mImageMenuRunnable.aboutToCall(item, mAllImages.getImageAt(mCurrentPosition));
+ return b;
+ }
+
+ /*
+ * Here's the loading strategy. For any given image, load the thumbnail
+ * into memory and post a callback to display the resulting bitmap.
+ *
+ * Then proceed to load the full image bitmap. Three things can
+ * happen at this point:
+ *
+ * 1. the image fails to load because the UI thread decided
+ * to move on to a different image. This "cancellation" happens
+ * by virtue of the UI thread closing the stream containing the
+ * image being decoded. BitmapFactory.decodeStream returns null
+ * in this case.
+ *
+ * 2. the image loaded successfully. At that point we post
+ * a callback to the UI thread to actually show the bitmap.
+ *
+ * 3. when the post runs it checks to see if the image that was
+ * loaded is still the one we want. The UI may have moved on
+ * to some other image and if so we just drop the newly loaded
+ * bitmap on the floor.
+ */
+
+ interface ImageGetterCallback {
+ public void imageLoaded(int pos, int offset, Bitmap bitmap, boolean isThumb);
+ public boolean wantsThumbnail(int pos, int offset);
+ public boolean wantsFullImage(int pos, int offset);
+ public int fullImageSizeToUse(int pos, int offset);
+ public void completed(boolean wasCanceled);
+ public int [] loadOrder();
+ }
+
+ class ImageGetter {
+ // The thread which does the work.
+ private Thread mGetterThread;
+
+ // The base position that's being retrieved. The actual images retrieved
+ // are this base plus each of the offets.
+ private int mCurrentPosition = -1;
+
+ // The callback to invoke for each image.
+ private ImageGetterCallback mCB;
+
+ // This is the loader cancelable that gets set while we're loading an image.
+ // If we change position we can cancel the current load using this.
+ private ImageManager.IGetBitmap_cancelable mLoad;
+
+ // True if we're canceling the current load.
+ private boolean mCancelCurrent = false;
+
+ // True when the therad should exit.
+ private boolean mDone = false;
+
+ // True when the loader thread is waiting for work.
+ private boolean mReady = false;
+
+ private void cancelCurrent() {
+ synchronized (this) {
+ if (!mReady) {
+ mCancelCurrent = true;
+ ImageManager.IGetBitmap_cancelable load = mLoad;
+ if (load != null) {
+ if (Config.LOGV)
+ Log.v(TAG, "canceling load object");
+ load.cancel();
+ }
+ mCancelCurrent = false;
+ }
+ }
+ }
+
+ public ImageGetter() {
+ mGetterThread = new Thread(new Runnable() {
+
+ private Runnable callback(final int position, final int offset, final boolean isThumb, final Bitmap bitmap) {
+ return new Runnable() {
+ public void run() {
+ // check for inflight callbacks that aren't applicable any longer
+ // before delivering them
+ if (!isCanceled() && position == mCurrentPosition) {
+ mCB.imageLoaded(position, offset, bitmap, isThumb);
+ } else {
+ if (bitmap != null)
+ bitmap.recycle();
+ }
+ }
+ };
+ }
+
+ private Runnable completedCallback(final boolean wasCanceled) {
+ return new Runnable() {
+ public void run() {
+ mCB.completed(wasCanceled);
+ }
+ };
+ }
+
+ public void run() {
+ int lastPosition = -1;
+ while (!mDone) {
+ synchronized (ImageGetter.this) {
+ mReady = true;
+ ImageGetter.this.notify();
+
+ if (mCurrentPosition == -1 || lastPosition == mCurrentPosition) {
+ try {
+ ImageGetter.this.wait();
+ } catch (InterruptedException ex) {
+ continue;
+ }
+ }
+
+ lastPosition = mCurrentPosition;
+ mReady = false;
+ }
+
+ if (lastPosition != -1) {
+ int imageCount = mAllImages.getCount();
+
+ int [] order = mCB.loadOrder();
+ for (int i = 0; i < order.length; i++) {
+ int offset = order[i];
+ int imageNumber = lastPosition + offset;
+ if (imageNumber >= 0 && imageNumber < imageCount) {
+ ImageManager.IImage image = mAllImages.getImageAt(lastPosition + offset);
+ if (image == null || isCanceled()) {
+ break;
+ }
+ if (mCB.wantsThumbnail(lastPosition, offset)) {
+ if (Config.LOGV)
+ Log.v(TAG, "starting THUMBNAIL load at offset " + offset);
+ Bitmap b = image.thumbBitmap();
+ mHandler.post(callback(lastPosition, offset, true, b));
+ }
+ }
+ }
+
+ for (int i = 0; i < order.length; i++) {
+ int offset = order[i];
+ int imageNumber = lastPosition + offset;
+ if (imageNumber >= 0 && imageNumber < imageCount) {
+ ImageManager.IImage image = mAllImages.getImageAt(lastPosition + offset);
+ if (mCB.wantsFullImage(lastPosition, offset)) {
+ if (Config.LOGV)
+ Log.v(TAG, "starting FULL IMAGE load at offset " + offset);
+ int sizeToUse = mCB.fullImageSizeToUse(lastPosition, offset);
+ if (image != null && !isCanceled()) {
+ mLoad = image.fullSizeBitmap_cancelable(sizeToUse);
+ }
+ if (mLoad != null) {
+ long t1;
+ if (Config.LOGV) t1 = System.currentTimeMillis();
+
+ Bitmap b = null;
+ try {
+ b = mLoad.get();
+ } catch (OutOfMemoryError e) {
+ Log.e(TAG, "couldn't load full size bitmap for " + "");
+ }
+ if (Config.LOGV && b != null) {
+ long t2 = System.currentTimeMillis();
+ Log.v(TAG, "loading full image for " + image.fullSizeImageUri()
+ + " with requested size " + sizeToUse
+ + " took " + (t2-t1)
+ + " and returned a bitmap with size "
+ + b.getWidth() + " / " + b.getHeight());
+ }
+
+ mLoad = null;
+ if (b != null) {
+ if (isCanceled()) {
+ b.recycle();
+ } else {
+ mHandler.post(callback(lastPosition, offset, false, b));
+ }
+ }
+ }
+ }
+ }
+ }
+ mHandler.post(completedCallback(isCanceled()));
+ }
+ }
+ }
+ });
+ mGetterThread.setName("ImageGettter");
+ mGetterThread.start();
+ }
+
+ private boolean isCanceled() {
+ synchronized (this) {
+ return mCancelCurrent;
+ }
+ }
+
+ public void setPosition(int position, ImageGetterCallback cb) {
+ synchronized (this) {
+ if (!mReady) {
+ try {
+ mCancelCurrent = true;
+ ImageManager.IGetBitmap_cancelable load = mLoad;
+ if (load != null) {
+ load.cancel();
+ }
+ // if the thread is waiting before loading the full size
+ // image then this will free it up
+ ImageGetter.this.notify();
+ ImageGetter.this.wait();
+ mCancelCurrent = false;
+ } catch (InterruptedException ex) {
+ // not sure what to do here
+ }
+ }
+ }
+
+ mCurrentPosition = position;
+ mCB = cb;
+
+ synchronized (this) {
+ ImageGetter.this.notify();
+ }
+ }
+
+ public void stop() {
+ synchronized (this) {
+ mDone = true;
+ ImageGetter.this.notify();
+ }
+ try {
+ mGetterThread.join();
+ } catch (InterruptedException ex) {
+
+ }
+ }
+ }
+
+ private void setImage(int pos) {
+ if (!mLayoutComplete) {
+ return;
+ }
+
+ final boolean left = mCurrentPosition > pos;
+
+ mCurrentPosition = pos;
+
+ ImageViewTouchBase current = mImageViews[1];
+ current.mSuppMatrix.reset();
+ current.setImageMatrix(current.getImageViewMatrix());
+
+ if (false) {
+ Log.v(TAG, "before...");
+ for (ImageViewTouchBase ivtb : mImageViews)
+ ivtb.dump();
+ }
+
+ if (!mFirst) {
+ if (left) {
+ mImageViews[2].copyFrom(mImageViews[1]);
+ mImageViews[1].copyFrom(mImageViews[0]);
+ } else {
+ mImageViews[0].copyFrom(mImageViews[1]);
+ mImageViews[1].copyFrom(mImageViews[2]);
+ }
+ }
+ if (false) {
+ Log.v(TAG, "after copy...");
+ for (ImageViewTouchBase ivtb : mImageViews)
+ ivtb.dump();
+ }
+
+ for (ImageViewTouchBase ivt: mImageViews) {
+ ivt.mIsZooming = false;
+ }
+ int width = mImageViews[1].getWidth();
+ int from;
+ int to = width + sPadding;
+ if (mFirst) {
+ from = to;
+ mFirst = false;
+ } else {
+ from = left ? (width + sPadding) + mScroller.getScrollX()
+ : mScroller.getScrollX() - (width + sPadding);
+ }
+
+ if (sAnimateTransitions) {
+ mScroller.scrollTo(from, 0);
+ animateScrollTo(to, 0);
+ } else {
+ mScroller.scrollTo(to, 0);
+ }
+
+ ImageGetterCallback cb = new ImageGetterCallback() {
+ public void completed(boolean wasCanceled) {
+ mImageViews[1].setFocusableInTouchMode(true);
+ mImageViews[1].requestFocus();
+ }
+
+ public boolean wantsThumbnail(int pos, int offset) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ return ivt.mThumbBitmap == null;
+ }
+
+ public boolean wantsFullImage(int pos, int offset) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ if (ivt.mBitmapDisplayed != null && !ivt.mBitmapIsThumbnail) {
+ return false;
+ }
+ if (offset != 0) {
+ return false;
+ }
+ return true;
+ }
+
+ public int fullImageSizeToUse(int pos, int offset) {
+ // TODO
+ // this number should be bigger so that we can zoom. we may need to
+ // get fancier and read in the fuller size image as the user starts
+ // to zoom. use -1 to get the full full size image.
+ // for now use 480 so we don't run out of memory
+ final int imageViewSize = 480;
+ return imageViewSize;
+ }
+
+ public int [] loadOrder() {
+ return sOrder_adjacents;
+ }
+
+ public void imageLoaded(int pos, int offset, Bitmap bitmap, boolean isThumb) {
+ ImageViewTouchBase ivt = mImageViews[1 + offset];
+ ivt.setImageBitmapResetBase(bitmap, isThumb, isThumb);
+ }
+ };
+
+ // Could be null if we're stopping a slide show in the course of pausing
+ if (mGetter != null) {
+ mGetter.setPosition(pos, cb);
+ }
+ showOnScreenControls();
+ }
+
+ @Override
+ public void onCreate(Bundle instanceState)
+ {
+ super.onCreate(instanceState);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ setContentView(R.layout.viewimage);
+
+ mImageViews[0] = (ImageViewTouch) findViewById(R.id.image1);
+ mImageViews[1] = (ImageViewTouch) findViewById(R.id.image2);
+ mImageViews[2] = (ImageViewTouch) findViewById(R.id.image3);
+
+ mScroller = (ScrollHandler)findViewById(R.id.scroller);
+ makeGetter();
+
+ mAnimationIndex = -1;
+
+ mSlideShowInAnimation = new Animation[] {
+ makeInAnimation(R.anim.transition_in),
+ makeInAnimation(R.anim.slide_in),
+ makeInAnimation(R.anim.slide_in_vertical),
+ };
+
+ mSlideShowOutAnimation = new Animation[] {
+ makeOutAnimation(R.anim.transition_out),
+ makeOutAnimation(R.anim.slide_out),
+ makeOutAnimation(R.anim.slide_out_vertical),
+ };
+
+ mSlideShowImageViews[0] = (ImageViewTouch) findViewById(R.id.image1_slideShow);
+ mSlideShowImageViews[1] = (ImageViewTouch) findViewById(R.id.image2_slideShow);
+ for (int i = 0; i < mSlideShowImageViews.length; i++) {
+ mSlideShowImageViews[i].setImageBitmapResetBase(null, true, true);
+ mSlideShowImageViews[i].setVisibility(View.INVISIBLE);
+ }
+
+ Uri uri = getIntent().getData();
+
+ if (Config.LOGV)
+ Log.v(TAG, "uri is " + uri);
+ if (instanceState != null) {
+ if (instanceState.containsKey("uri")) {
+ uri = Uri.parse(instanceState.getString("uri"));
+ }
+ }
+ if (uri == null) {
+ finish();
+ return;
+ }
+ init(uri);
+
+ Bundle b = getIntent().getExtras();
+ boolean slideShow = b != null ? b.getBoolean("slideshow", false) : false;
+ if (slideShow) {
+ setMode(MODE_SLIDESHOW);
+ loadNextImage(mCurrentPosition, 0, true);
+ }
+
+ // Get the zoom controls and add them to the bottom of the map
+ View zoomControls = getZoomControls();
+ RelativeLayout root = (RelativeLayout) findViewById(R.id.rootLayout);
+ RelativeLayout.LayoutParams p = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ p.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ p.addRule(RelativeLayout.CENTER_HORIZONTAL);
+ root.addView(zoomControls, p);
+
+ mNextImageView = findViewById(R.id.next_image);
+ mPrevImageView = findViewById(R.id.prev_image);
+
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+
+ private Animation makeInAnimation(int id) {
+ Animation inAnimation = AnimationUtils.loadAnimation(this, id);
+ return inAnimation;
+ }
+
+ private Animation makeOutAnimation(int id) {
+ Animation outAnimation = AnimationUtils.loadAnimation(this, id);
+ return outAnimation;
+ }
+
+ private void setMode(int mode) {
+ if (mMode == mode) {
+ return;
+ }
+
+ findViewById(R.id.slideShowContainer).setVisibility(mode == MODE_SLIDESHOW ? View.VISIBLE : View.GONE);
+ findViewById(R.id.abs) .setVisibility(mode == MODE_NORMAL ? View.VISIBLE : View.GONE);
+
+ Window win = getWindow();
+ mMode = mode;
+ if (mode == MODE_SLIDESHOW) {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ if (sSlideShowHidesStatusBar) {
+ win.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ for (ImageViewTouchBase ivt: mImageViews) {
+ ivt.clear();
+ }
+
+ if (false) {
+ Log.v(TAG, "current is " + this.mSlideShowImageCurrent);
+ this.mSlideShowImageViews[0].dump();
+ this.mSlideShowImageViews[0].dump();
+ }
+
+ findViewById(R.id.slideShowContainer).getRootView().requestLayout();
+ mUseShuffleOrder = mPrefs.getBoolean("pref_gallery_slideshow_shuffle_key", false);
+ mSlideShowLoop = mPrefs.getBoolean("pref_gallery_slideshow_repeat_key", false);
+ try {
+ mAnimationIndex = Integer.parseInt(mPrefs.getString("pref_gallery_slideshow_transition_key", "0"));
+ } catch (Exception ex) {
+ Log.e(TAG, "couldn't parse preference: " + ex.toString());
+ mAnimationIndex = 0;
+ }
+ try {
+ mSlideShowInterval = Integer.parseInt(mPrefs.getString("pref_gallery_slideshow_interval_key", "3")) * 1000;
+ } catch (Exception ex) {
+ Log.e(TAG, "couldn't parse preference: " + ex.toString());
+ mSlideShowInterval = 3000;
+ }
+
+ if (Config.LOGV) {
+ Log.v(TAG, "read prefs... shuffle: " + mUseShuffleOrder);
+ Log.v(TAG, "read prefs... loop: " + mSlideShowLoop);
+ Log.v(TAG, "read prefs... animidx: " + mAnimationIndex);
+ Log.v(TAG, "read prefs... interval: " + mSlideShowInterval);
+ }
+
+ if (mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+ } else {
+ if (Config.LOGV)
+ Log.v(TAG, "slide show mode off, mCurrentPosition == " + mCurrentPosition);
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ if (mGetter != null)
+ mGetter.cancelCurrent();
+
+ if (sSlideShowHidesStatusBar) {
+ win.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ ImageViewTouchBase dst = mImageViews[1];
+ dst.mLastXTouchPos = -1;
+ dst.mLastYTouchPos = -1;
+
+ for (ImageViewTouchBase ivt: mSlideShowImageViews) {
+ ivt.clear();
+ }
+
+ mShuffleOrder = null;
+
+ // mGetter null is a proxy for being paused
+ if (mGetter != null) {
+ mFirst = true; // don't animate
+ setImage(mCurrentPosition);
+ }
+ }
+
+ // this line shouldn't be necessary but the view hierarchy doesn't
+ // seem to realize that the window layout changed
+ mScroller.requestLayout();
+ }
+
+ private void generateShuffleOrder() {
+ if (mShuffleOrder == null || mShuffleOrder.length != mAllImages.getCount()) {
+ mShuffleOrder = new int[mAllImages.getCount()];
+ }
+
+ for (int i = 0; i < mShuffleOrder.length; i++) {
+ mShuffleOrder[i] = i;
+ }
+
+ for (int i = mShuffleOrder.length - 1; i > 0; i--) {
+ int r = mRandom.nextInt(i);
+ int tmp = mShuffleOrder[r];
+ mShuffleOrder[r] = mShuffleOrder[i];
+ mShuffleOrder[i] = tmp;
+ }
+ }
+
+ private void loadNextImage(final int requestedPos, final long delay, final boolean firstCall) {
+ if (firstCall && mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+
+ final long targetDisplayTime = System.currentTimeMillis() + delay;
+
+ ImageGetterCallback cb = new ImageGetterCallback() {
+ public void completed(boolean wasCanceled) {
+ }
+
+ public boolean wantsThumbnail(int pos, int offset) {
+ return true;
+ }
+
+ public boolean wantsFullImage(int pos, int offset) {
+ return false;
+ }
+
+ public int [] loadOrder() {
+ return sOrder_slideshow;
+ }
+
+ public int fullImageSizeToUse(int pos, int offset) {
+ return 480; // TODO compute this
+ }
+
+ public void imageLoaded(final int pos, final int offset, final Bitmap bitmap, final boolean isThumb) {
+ long timeRemaining = Math.max(0, targetDisplayTime - System.currentTimeMillis());
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ if (mMode == MODE_NORMAL) {
+ return;
+ }
+
+ ImageViewTouchBase oldView = mSlideShowImageViews[mSlideShowImageCurrent];
+
+ if (++mSlideShowImageCurrent == mSlideShowImageViews.length) {
+ mSlideShowImageCurrent = 0;
+ }
+
+ ImageViewTouchBase newView = mSlideShowImageViews[mSlideShowImageCurrent];
+ newView.setVisibility(View.VISIBLE);
+ newView.setImageBitmapResetBase(bitmap, isThumb, isThumb);
+ newView.bringToFront();
+
+ int animation = 0;
+
+ if (mAnimationIndex == -1) {
+ int n = mRandom.nextInt(mSlideShowInAnimation.length);
+ animation = n;
+ } else {
+ animation = mAnimationIndex;
+ }
+
+ Animation aIn = mSlideShowInAnimation[animation];
+ newView.setAnimation(aIn);
+ newView.setVisibility(View.VISIBLE);
+ aIn.startNow();
+
+ Animation aOut = mSlideShowOutAnimation[animation];
+ oldView.setVisibility(View.INVISIBLE);
+ oldView.setAnimation(aOut);
+ aOut.startNow();
+
+ mCurrentPosition = requestedPos;
+
+ mHandler.post(new Runnable() {
+ public void run() {
+ if (mCurrentPosition == mLastSlideShowImage && !firstCall) {
+ if (mSlideShowLoop) {
+ if (mUseShuffleOrder) {
+ generateShuffleOrder();
+ }
+ } else {
+ setMode(MODE_NORMAL);
+ return;
+ }
+ }
+
+ if (Config.LOGV)
+ Log.v(TAG, "mCurrentPosition is now " + mCurrentPosition);
+ loadNextImage((mCurrentPosition + 1) % mAllImages.getCount(), mSlideShowInterval, false);
+ }
+ });
+ }
+ }, timeRemaining);
+ }
+ };
+ // Could be null if we're stopping a slide show in the course of pausing
+ if (mGetter != null) {
+ int pos = requestedPos;
+ if (mShuffleOrder != null) {
+ pos = mShuffleOrder[pos];
+ }
+ mGetter.setPosition(pos, cb);
+ }
+ }
+
+ private void makeGetter() {
+ mGetter = new ImageGetter();
+ }
+
+ private void init(Uri uri) {
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ mSortAscending = false;
+ if (sortOrder != null) {
+ mSortAscending = sortOrder.equals("ascending");
+ }
+ int sort = mSortAscending ? ImageManager.SORT_ASCENDING : ImageManager.SORT_DESCENDING;
+ mAllImages = ImageManager.makeImageList(uri, this, sort);
+
+ uri = uri.buildUpon().query(null).build();
+ // TODO smarter/faster here please
+ for (int i = 0; i < mAllImages.getCount(); i++) {
+ ImageManager.IImage image = mAllImages.getImageAt(i);
+ if (image.fullSizeImageUri().equals(uri)) {
+ mCurrentPosition = i;
+ mLastSlideShowImage = mCurrentPosition;
+ break;
+ }
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle b) {
+ super.onSaveInstanceState(b);
+ ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition);
+
+ if (image != null){
+ Uri uri = image.fullSizeImageUri();
+ String bucket = null;
+ if(getIntent()!= null && getIntent().getData()!=null)
+ bucket = getIntent().getData().getQueryParameter("bucketId");
+
+ if(bucket!=null)
+ uri = uri.buildUpon().appendQueryParameter("bucketId", bucket).build();
+
+ b.putString("uri", uri.toString());
+ }
+ if (mMode == MODE_SLIDESHOW)
+ b.putBoolean("slideshow", true);
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+
+ ImageManager.IImage image = mAllImages.getImageAt(mCurrentPosition);
+
+ String sortOrder = mPrefs.getString("pref_gallery_sort_key", null);
+ boolean sortAscending = false;
+ if (sortOrder != null) {
+ sortAscending = sortOrder.equals("ascending");
+ }
+ if (sortAscending != mSortAscending) {
+ init(image.fullSizeImageUri());
+ }
+
+ if (mGetter == null) {
+ makeGetter();
+ }
+
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.setImageBitmap(null, true);
+ }
+
+ mFirst = true;
+ mScroller.setLayoutCompletedCallback(new Runnable() {
+ public void run() {
+ mLayoutComplete = true;
+ setImage(mCurrentPosition);
+ }
+ });
+ setImage(mCurrentPosition);
+
+ // normally this will never be zero but if one "backs" into this
+ // activity after removing the sdcard it could be zero. in that
+ // case just "finish" since there's nothing useful that can happen.
+ if (mAllImages.getCount() == 0) {
+ finish();
+ } else {
+ MenuHelper.requestOrientation(this, mPrefs);
+ }
+ }
+
+ @Override
+ public void onPause()
+ {
+ super.onPause();
+
+ mGetter.cancelCurrent();
+ mGetter.stop();
+ mGetter = null;
+ setMode(MODE_NORMAL);
+
+ mAllImages.deactivate();
+
+ for (ImageViewTouchBase iv: mImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+
+ for (ImageViewTouchBase iv: mSlideShowImageViews) {
+ iv.recycleBitmaps();
+ iv.setImageBitmap(null, true);
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ }
+}
diff --git a/src/com/android/camera/ViewVideo.java b/src/com/android/camera/ViewVideo.java
new file mode 100644
index 0000000..527f0bb
--- /dev/null
+++ b/src/com/android/camera/ViewVideo.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2008 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.media.MediaPlayer;
+import android.app.Activity;
+import android.os.Bundle;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.Menu;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.MediaController;
+import android.view.Window;
+import android.widget.VideoView;
+import android.util.Config;
+
+class ViewVideo extends Activity
+{
+ static final String TAG = "ViewVideo";
+
+ private ImageManager.IImageList mAllVideos;
+ private PowerManager.WakeLock mWakeLock;
+ private ContentResolver mContentResolver;
+ private VideoView mVideoView;
+ private ImageManager.IImage mVideo;
+ private int mCurrentPosition = -1;
+ private MediaController mMediaController;
+
+ // if the activity gets paused the stash the current position here
+ int mPausedPlaybackPosition = 0;
+
+
+ public ViewVideo()
+ {
+ }
+
+ @Override
+ public void onCreate(Bundle icicle)
+ {
+ super.onCreate(icicle);
+ if (Config.LOGV)
+ Log.v(TAG, "onCreate");
+ //getWindow().setFormat(android.graphics.PixelFormat.TRANSLUCENT);
+
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+
+ mContentResolver = getContentResolver();
+
+ setDefaultKeyMode(DEFAULT_KEYS_SHORTCUT);
+ setContentView(R.layout.viewvideo);
+
+ mMediaController = new MediaController(this);
+ mVideoView = (VideoView) findViewById(R.id.video);
+ mVideoView.setMediaController(mMediaController);
+ mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ // TODO what do we really want to do at the end of playback?
+ finish();
+ }
+ });
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle b) {
+ if (Config.LOGV)
+ Log.v(TAG, "onSaveInstanceState");
+ b.putInt("playback_position", mPausedPlaybackPosition);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle b) {
+ if (Config.LOGV)
+ Log.v(TAG, "onRestoreInstanceState");
+ mPausedPlaybackPosition = b.getInt("playback_position", 0);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (Config.LOGV)
+ Log.v(TAG, "onPause");
+ mAllVideos.deactivate();
+
+ mVideoView.pause();
+ mPausedPlaybackPosition = mVideoView.getCurrentPosition();
+ mVideoView.setVideoURI(null);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (Config.LOGV)
+ Log.v(TAG, "onStop");
+ }
+
+ @Override
+ public void onResume()
+ {
+ super.onResume();
+ if (Config.LOGV)
+ Log.v(TAG, "onResume");
+
+ mAllVideos = ImageManager.instance().allImages(
+ ViewVideo.this,
+ mContentResolver,
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_DESCENDING);
+
+ // TODO smarter/faster here please
+ Uri uri = getIntent().getData();
+ if (mVideo == null) {
+ for (int i = 0; i < mAllVideos.getCount(); i++) {
+ ImageManager.IImage video = mAllVideos.getImageAt(i);
+ if (video.fullSizeImageUri().equals(uri)) {
+ mCurrentPosition = i;
+ mVideo = video;
+ break;
+ }
+ }
+ }
+
+ if (mCurrentPosition != -1) {
+ mMediaController.setPrevNextListeners(
+ new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ if (++mCurrentPosition == mAllVideos.getCount())
+ mCurrentPosition = 0;
+ ImageManager.IImage video = mAllVideos.getImageAt(mCurrentPosition);
+ mVideo = video;
+ mVideoView.setVideoURI(video.fullSizeImageUri());
+ mVideoView.start();
+ }
+ },
+ new android.view.View.OnClickListener() {
+ public void onClick(View v) {
+ if (--mCurrentPosition == -1)
+ mCurrentPosition = mAllVideos.getCount() - 1;
+ ImageManager.IImage video = mAllVideos.getImageAt(mCurrentPosition);
+ mVideo = video;
+ mVideoView.setVideoURI(video.fullSizeImageUri());
+ mVideoView.start();
+ }
+ });
+ }
+ if (Config.LOGV)
+ android.util.Log.v("camera", "seekTo " + mPausedPlaybackPosition);
+ mVideoView.setVideoURI(uri);
+ mVideoView.seekTo(mPausedPlaybackPosition);
+ mVideoView.start();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu)
+ {
+ super.onCreateOptionsMenu(menu);
+ MenuHelper.addVideoMenuItems(
+ menu,
+ MenuHelper.INCLUDE_ALL & ~MenuHelper.INCLUDE_VIEWPLAY_MENU,
+ ViewVideo.this, // activity
+ null, // handler
+ new SelectedImageGetter() {
+ public ImageManager.IImage getCurrentImage() {
+ return mVideo;
+ }
+ public Uri getCurrentImageUri() {
+ return mVideo.fullSizeImageUri();
+ }
+ },
+
+ // deletion case
+ new Runnable() {
+ public void run() {
+ mAllVideos.removeImage(mVideo);
+ finish();
+ }
+ },
+
+ // pre-work
+ null,
+
+ // post-work
+ null);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu)
+ {
+ return super.onPrepareOptionsMenu(menu);
+ }
+}
diff --git a/src/com/android/camera/Wallpaper.java b/src/com/android/camera/Wallpaper.java
new file mode 100644
index 0000000..ff41242
--- /dev/null
+++ b/src/com/android/camera/Wallpaper.java
@@ -0,0 +1,193 @@
+/*
+ * 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 android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+/**
+ * Wallpaper picker for the camera application. This just redirects to the standard pick action.
+ */
+public class Wallpaper extends Activity {
+ private static final String LOG_TAG = "Camera";
+ static final int PHOTO_PICKED = 1;
+ static final int CROP_DONE = 2;
+
+ static final int SHOW_PROGRESS = 0;
+ static final int FINISH = 1;
+
+ static final String sDoLaunchIcicle = "do_launch";
+ static final String sTempFilePathIcicle = "temp_file_path";
+
+ private ProgressDialog mProgressDialog = null;
+ private boolean mDoLaunch = true;
+ private String mTempFilePath;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW_PROGRESS: {
+ CharSequence c = getText(R.string.wallpaper);
+ mProgressDialog = ProgressDialog.show(Wallpaper.this, "", c, true, false);
+ break;
+ }
+
+ case FINISH: {
+ closeProgressDialog();
+ setResult(RESULT_OK);
+ finish();
+ break;
+ }
+ }
+ }
+ };
+
+ static class SetWallpaperThread extends Thread {
+ private Bitmap mBitmap;
+ private Handler mHandler;
+ private Context mContext;
+
+ public SetWallpaperThread(Bitmap bitmap, Handler handler, Context context) {
+ mBitmap = bitmap;
+ mHandler = handler;
+ mContext = context;
+ }
+
+ @Override
+ public void run() {
+ try {
+ mContext.setWallpaper(mBitmap);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Failed to set wallpaper.", e);
+ } finally {
+ mHandler.sendEmptyMessage(FINISH);
+ }
+ }
+ }
+
+ private synchronized void closeProgressDialog() {
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss();
+ mProgressDialog = null;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ if (icicle != null) {
+ mDoLaunch = icicle.getBoolean(sDoLaunchIcicle);
+ mTempFilePath = icicle.getString(sTempFilePathIcicle);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle icicle) {
+ icicle.putBoolean(sDoLaunchIcicle, mDoLaunch);
+ icicle.putString(sTempFilePathIcicle, mTempFilePath);
+ }
+
+ @Override
+ protected void onPause() {
+ closeProgressDialog();
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (!mDoLaunch) {
+ return;
+ }
+ Uri imageToUse = getIntent().getData();
+ if (imageToUse != null) {
+ Intent intent = new Intent();
+ intent.setClassName("com.android.camera", "com.android.camera.CropImage");
+ intent.setData(imageToUse);
+ formatIntent(intent);
+ startActivityForResult(intent, CROP_DONE);
+ } else {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
+ intent.setType("image/*");
+ intent.putExtra("crop", "true");
+ formatIntent(intent);
+ startActivityForResult(intent, PHOTO_PICKED);
+ }
+ }
+
+ protected void formatIntent(Intent intent) {
+ File f = getFileStreamPath("temp-wallpaper");
+ (new File(f.getParent())).mkdirs();
+ mTempFilePath = f.toString();
+ f.delete();
+
+ int width = getWallpaperDesiredMinimumWidth();
+ int height = getWallpaperDesiredMinimumHeight();
+ intent.putExtra("outputX", width);
+ intent.putExtra("outputY", height);
+ intent.putExtra("aspectX", width);
+ intent.putExtra("aspectY", height);
+ intent.putExtra("scale", true);
+ intent.putExtra("noFaceDetection", true);
+ intent.putExtra("output", Uri.parse("file:/" + mTempFilePath));
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if ((requestCode == PHOTO_PICKED || requestCode == CROP_DONE) && (resultCode == RESULT_OK)
+ && (data != null)) {
+ try {
+ InputStream s = new FileInputStream(mTempFilePath);
+ Bitmap bitmap = BitmapFactory.decodeStream(s);
+ if (bitmap == null) {
+ Log.e(LOG_TAG, "Failed to set wallpaper. Couldn't get bitmap for path " + mTempFilePath);
+ } else {
+ if (android.util.Config.LOGV)
+ Log.v(LOG_TAG, "bitmap size is " + bitmap.getWidth() + " / " + bitmap.getHeight());
+ mHandler.sendEmptyMessage(SHOW_PROGRESS);
+ new SetWallpaperThread(bitmap, mHandler, this).start();
+ }
+ mDoLaunch = false;
+ } catch (FileNotFoundException ex) {
+
+ } catch (IOException ex) {
+
+ }
+ } else {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+}
diff --git a/src/com/android/camera/YouTubeUpload.java b/src/com/android/camera/YouTubeUpload.java
new file mode 100644
index 0000000..0963d09
--- /dev/null
+++ b/src/com/android/camera/YouTubeUpload.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2006 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.Intent;
+import android.net.Uri;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.widget.TextView;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+import android.util.Log;
+import android.util.Xml;
+
+
+public class YouTubeUpload extends Activity
+{
+ private static final String TAG = "YouTubeUpload";
+
+ private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
+ private static final boolean mDevServer = false;
+
+ private ArrayList<String> mCategoriesShort = new ArrayList<String>();
+ private ArrayList<String> mCategoriesLong = new ArrayList<String>();
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ TextView tv = new TextView(this);
+ tv.setText("");
+ setContentView(tv);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ final ProgressDialog pd = ProgressDialog.show(this, "please wait", "");
+
+ final ImageManager.IImageList all = ImageManager.instance().allImages(
+ YouTubeUpload.this,
+ getContentResolver(),
+ ImageManager.DataLocation.ALL,
+ ImageManager.INCLUDE_VIDEOS,
+ ImageManager.SORT_ASCENDING);
+
+ android.net.Uri uri = getIntent().getData();
+ if (uri == null) {
+ uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM);
+ }
+ if (uri != null) {
+ final ImageManager.VideoObject vid = (ImageManager.VideoObject) all.getImageForUri(uri);
+ if (vid != null) {
+ new Thread(new Runnable() {
+ public void run() {
+ getCategories();
+ runOnUiThread(new Runnable() {
+ public void run() {
+ pd.cancel();
+ MenuHelper.YouTubeUploadInfoDialog infoDialog = new MenuHelper.YouTubeUploadInfoDialog(
+ YouTubeUpload.this,
+ mCategoriesShort,
+ mCategoriesLong,
+ vid,
+ new Runnable() {
+ public void run() {
+ finish();
+ }
+ });
+ infoDialog.show();
+ }
+ });
+ }
+ }).start();
+ }
+ }
+ }
+
+ protected String getYouTubeBaseUrl() {
+ if (mDevServer) {
+ return "http://dev.gdata.youtube.com";
+ } else {
+ return "http://gdata.youtube.com";
+ }
+ }
+
+ public void getCategories() {
+ String uri = getYouTubeBaseUrl() + "/schemas/2007/categories.cat";
+ AndroidHttpClient mClient = AndroidHttpClient.newInstance("Android-Camera/0.1");
+
+ try {
+ org.apache.http.HttpResponse r = mClient.execute(new org.apache.http.client.methods.HttpGet(uri));
+ processReturnedData(r.getEntity().getContent());
+ } catch (Exception ex) {
+ Log.e(TAG, "got exception getting categories... " + ex.toString());
+ }
+ }
+
+ public void processReturnedData(InputStream s) throws IOException, SAXException, XmlPullParserException {
+ try {
+ Xml.parse(s, Xml.findEncodingByName(null), new DefaultHandler() {
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ if (ATOM_NAMESPACE.equals(uri)) {
+ if ("category".equals(localName)) {
+ String catShortName = attributes.getValue("", "term");
+ String catLongName = attributes.getValue("", "label");
+ mCategoriesLong .add(catLongName);
+ mCategoriesShort.add(catShortName);
+ return;
+ }
+ }
+ }
+ });
+ } catch (SAXException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..168bcd9
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,18 @@
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+LOCAL_CERTIFICATE := media
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := googlelogin-client
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CameraTests
+
+LOCAL_INSTRUMENTATION_FOR := Camera
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..b8074f5
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.camera.tests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="CameraLaunchPerformance"
+ android:targetPackage="com.android.camera"
+ android:label="Camera Launch Performance">
+ </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/camera/CameraLaunchPerformance.java b/tests/src/com/android/camera/CameraLaunchPerformance.java
new file mode 100644
index 0000000..8c76f00
--- /dev/null
+++ b/tests/src/com/android/camera/CameraLaunchPerformance.java
@@ -0,0 +1,53 @@
+/*
+ * 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.tests;
+
+import android.app.Activity;
+import android.test.LaunchPerformanceBase;
+import android.os.Bundle;
+
+import java.util.Map;
+
+/**
+ * Instrumentation class for Camera launch performance testing.
+ */
+public class CameraLaunchPerformance extends LaunchPerformanceBase {
+
+ public static final String LOG_TAG = "CameraLaunchPerformance";
+
+ public CameraLaunchPerformance() {
+ super();
+ }
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+
+ mIntent.setClassName(getTargetContext(), "com.android.camera.Camera");
+ start();
+ }
+
+ /**
+ * Calls LaunchApp and finish.
+ */
+ @Override
+ public void onStart() {
+ super.onStart();
+ LaunchApp();
+ finish(Activity.RESULT_OK, mResults);
+ }
+}