diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | 1d4c75065966c4f6f56900e31f655bfd1b334435 (patch) | |
tree | 5d4526db3153daa63087fcb9384f8bc0659fbd18 | |
download | LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.zip LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.gz LegacyCamera-1d4c75065966c4f6f56900e31f655bfd1b334435.tar.bz2 |
Initial Contribution
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 @@ -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 Binary files differnew file mode 100644 index 0000000..a2bb888 --- /dev/null +++ b/res/drawable/app_camera.png diff --git a/res/drawable/app_photos.png b/res/drawable/app_photos.png Binary files differnew file mode 100644 index 0000000..971a41e --- /dev/null +++ b/res/drawable/app_photos.png 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 Binary files differnew file mode 100644 index 0000000..a9709aa --- /dev/null +++ b/res/drawable/btn_camera_arrow_left_default.png diff --git a/res/drawable/btn_camera_arrow_left_press.png b/res/drawable/btn_camera_arrow_left_press.png Binary files differnew file mode 100644 index 0000000..262c928 --- /dev/null +++ b/res/drawable/btn_camera_arrow_left_press.png 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 Binary files differnew file mode 100644 index 0000000..a4f78dc --- /dev/null +++ b/res/drawable/btn_camera_arrow_right_default.png diff --git a/res/drawable/btn_camera_arrow_right_press.png b/res/drawable/btn_camera_arrow_right_press.png Binary files differnew file mode 100644 index 0000000..4b37b0d --- /dev/null +++ b/res/drawable/btn_camera_arrow_right_press.png diff --git a/res/drawable/camera_crop_height.png b/res/drawable/camera_crop_height.png Binary files differnew file mode 100755 index 0000000..b089aec --- /dev/null +++ b/res/drawable/camera_crop_height.png diff --git a/res/drawable/camera_crop_width.png b/res/drawable/camera_crop_width.png Binary files differnew file mode 100755 index 0000000..65216af --- /dev/null +++ b/res/drawable/camera_crop_width.png diff --git a/res/drawable/carousel_tray_bg.9.png b/res/drawable/carousel_tray_bg.9.png Binary files differnew file mode 100644 index 0000000..d51babe --- /dev/null +++ b/res/drawable/carousel_tray_bg.9.png diff --git a/res/drawable/counter_divider.png b/res/drawable/counter_divider.png Binary files differnew file mode 100644 index 0000000..ff35a08 --- /dev/null +++ b/res/drawable/counter_divider.png diff --git a/res/drawable/delete_image.png b/res/drawable/delete_image.png Binary files differnew file mode 100644 index 0000000..032e683 --- /dev/null +++ b/res/drawable/delete_image.png 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 Binary files differnew file mode 100755 index 0000000..5b92ee2 --- /dev/null +++ b/res/drawable/frame_gallery_preview_album.png diff --git a/res/drawable/frame_gallery_preview_album_mask.png b/res/drawable/frame_gallery_preview_album_mask.png Binary files differnew file mode 100755 index 0000000..b053f30 --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_mask.png diff --git a/res/drawable/frame_gallery_preview_album_pressed.png b/res/drawable/frame_gallery_preview_album_pressed.png Binary files differnew file mode 100755 index 0000000..e3575ff --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_pressed.png diff --git a/res/drawable/frame_gallery_preview_album_selected.png b/res/drawable/frame_gallery_preview_album_selected.png Binary files differnew file mode 100755 index 0000000..222a38a --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_selected.png diff --git a/res/drawable/frame_gallery_preview_album_sublevel.png b/res/drawable/frame_gallery_preview_album_sublevel.png Binary files differnew file mode 100755 index 0000000..489a4f4 --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_sublevel.png diff --git a/res/drawable/frame_gallery_preview_album_sublevel_mask.png b/res/drawable/frame_gallery_preview_album_sublevel_mask.png Binary files differnew file mode 100755 index 0000000..4eefa03 --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_sublevel_mask.png diff --git a/res/drawable/frame_gallery_preview_album_sublevel_pressed.png b/res/drawable/frame_gallery_preview_album_sublevel_pressed.png Binary files differnew file mode 100755 index 0000000..970993f --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_sublevel_pressed.png diff --git a/res/drawable/frame_gallery_preview_album_sublevel_selected.png b/res/drawable/frame_gallery_preview_album_sublevel_selected.png Binary files differnew file mode 100755 index 0000000..70330ce --- /dev/null +++ b/res/drawable/frame_gallery_preview_album_sublevel_selected.png diff --git a/res/drawable/frame_gallery_thumb_uploaded_overlay.png b/res/drawable/frame_gallery_thumb_uploaded_overlay.png Binary files differnew file mode 100755 index 0000000..85b1fc8 --- /dev/null +++ b/res/drawable/frame_gallery_thumb_uploaded_overlay.png diff --git a/res/drawable/frame_overlay_gallery_camera.png b/res/drawable/frame_overlay_gallery_camera.png Binary files differnew file mode 100755 index 0000000..a1fcfff --- /dev/null +++ b/res/drawable/frame_overlay_gallery_camera.png diff --git a/res/drawable/frame_overlay_gallery_downloaded.png b/res/drawable/frame_overlay_gallery_downloaded.png Binary files differnew file mode 100755 index 0000000..70853ec --- /dev/null +++ b/res/drawable/frame_overlay_gallery_downloaded.png diff --git a/res/drawable/frame_overlay_gallery_video.png b/res/drawable/frame_overlay_gallery_video.png Binary files differnew file mode 100755 index 0000000..5381146 --- /dev/null +++ b/res/drawable/frame_overlay_gallery_video.png 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 Binary files differnew file mode 100644 index 0000000..2a505b8 --- /dev/null +++ b/res/drawable/highlight_unfocus.9.png diff --git a/res/drawable/ic_camera_flash.png b/res/drawable/ic_camera_flash.png Binary files differnew file mode 100755 index 0000000..58265bf --- /dev/null +++ b/res/drawable/ic_camera_flash.png diff --git a/res/drawable/ic_camera_indicator_auto_focus_green.png b/res/drawable/ic_camera_indicator_auto_focus_green.png Binary files differnew file mode 100644 index 0000000..7bc09db --- /dev/null +++ b/res/drawable/ic_camera_indicator_auto_focus_green.png diff --git a/res/drawable/ic_camera_indicator_flash_auto.png b/res/drawable/ic_camera_indicator_flash_auto.png Binary files differnew file mode 100755 index 0000000..a2809f1 --- /dev/null +++ b/res/drawable/ic_camera_indicator_flash_auto.png diff --git a/res/drawable/ic_camera_indicator_flash_off.png b/res/drawable/ic_camera_indicator_flash_off.png Binary files differnew file mode 100755 index 0000000..9a28b10 --- /dev/null +++ b/res/drawable/ic_camera_indicator_flash_off.png diff --git a/res/drawable/ic_camera_indicator_flash_on.png b/res/drawable/ic_camera_indicator_flash_on.png Binary files differnew file mode 100755 index 0000000..01fb9a4 --- /dev/null +++ b/res/drawable/ic_camera_indicator_flash_on.png diff --git a/res/drawable/ic_camera_indicator_memory.9.png b/res/drawable/ic_camera_indicator_memory.9.png Binary files differnew file mode 100644 index 0000000..ba4e1f5 --- /dev/null +++ b/res/drawable/ic_camera_indicator_memory.9.png diff --git a/res/drawable/ic_camera_indicator_photo.png b/res/drawable/ic_camera_indicator_photo.png Binary files differnew file mode 100755 index 0000000..ea981f4 --- /dev/null +++ b/res/drawable/ic_camera_indicator_photo.png diff --git a/res/drawable/ic_camera_indicator_video.png b/res/drawable/ic_camera_indicator_video.png Binary files differnew file mode 100755 index 0000000..eaaa9e4 --- /dev/null +++ b/res/drawable/ic_camera_indicator_video.png diff --git a/res/drawable/ic_camera_select_image.png b/res/drawable/ic_camera_select_image.png Binary files differnew file mode 100755 index 0000000..848123c --- /dev/null +++ b/res/drawable/ic_camera_select_image.png diff --git a/res/drawable/ic_camera_select_video.png b/res/drawable/ic_camera_select_video.png Binary files differnew file mode 100755 index 0000000..8d63517 --- /dev/null +++ b/res/drawable/ic_camera_select_video.png diff --git a/res/drawable/ic_gallery_back_to_entry.png b/res/drawable/ic_gallery_back_to_entry.png Binary files differnew file mode 100755 index 0000000..ad297ae --- /dev/null +++ b/res/drawable/ic_gallery_back_to_entry.png diff --git a/res/drawable/ic_gallery_empty.png b/res/drawable/ic_gallery_empty.png Binary files differnew file mode 100755 index 0000000..b140f84 --- /dev/null +++ b/res/drawable/ic_gallery_empty.png diff --git a/res/drawable/ic_gallery_finish.png b/res/drawable/ic_gallery_finish.png Binary files differnew file mode 100755 index 0000000..550e86a --- /dev/null +++ b/res/drawable/ic_gallery_finish.png diff --git a/res/drawable/ic_gallery_picasa_upload.png b/res/drawable/ic_gallery_picasa_upload.png Binary files differnew file mode 100755 index 0000000..211f93e --- /dev/null +++ b/res/drawable/ic_gallery_picasa_upload.png diff --git a/res/drawable/ic_gallery_upload_to.png b/res/drawable/ic_gallery_upload_to.png Binary files differnew file mode 100755 index 0000000..b8a338f --- /dev/null +++ b/res/drawable/ic_gallery_upload_to.png diff --git a/res/drawable/ic_gallery_youtube_upload.png b/res/drawable/ic_gallery_youtube_upload.png Binary files differnew file mode 100755 index 0000000..21c3820 --- /dev/null +++ b/res/drawable/ic_gallery_youtube_upload.png diff --git a/res/drawable/ic_launcher_camera.png b/res/drawable/ic_launcher_camera.png Binary files differnew file mode 100755 index 0000000..9bb4c61 --- /dev/null +++ b/res/drawable/ic_launcher_camera.png diff --git a/res/drawable/ic_launcher_gallery.png b/res/drawable/ic_launcher_gallery.png Binary files differnew file mode 100755 index 0000000..965fb71 --- /dev/null +++ b/res/drawable/ic_launcher_gallery.png diff --git a/res/drawable/ic_menu_view_details.png b/res/drawable/ic_menu_view_details.png Binary files differnew file mode 100644 index 0000000..a5a184f --- /dev/null +++ b/res/drawable/ic_menu_view_details.png diff --git a/res/drawable/ic_video_empty.png b/res/drawable/ic_video_empty.png Binary files differnew file mode 100755 index 0000000..1209f86 --- /dev/null +++ b/res/drawable/ic_video_empty.png diff --git a/res/drawable/image_border_bg_focus_blue.9.png b/res/drawable/image_border_bg_focus_blue.9.png Binary files differnew file mode 100644 index 0000000..89debd2 --- /dev/null +++ b/res/drawable/image_border_bg_focus_blue.9.png diff --git a/res/drawable/image_border_bg_focus_blue9.png b/res/drawable/image_border_bg_focus_blue9.png Binary files differnew file mode 100644 index 0000000..89debd2 --- /dev/null +++ b/res/drawable/image_border_bg_focus_blue9.png diff --git a/res/drawable/image_border_bg_normal.9.png b/res/drawable/image_border_bg_normal.9.png Binary files differnew file mode 100644 index 0000000..18e3607 --- /dev/null +++ b/res/drawable/image_border_bg_normal.9.png diff --git a/res/drawable/image_border_bg_normal9.png b/res/drawable/image_border_bg_normal9.png Binary files differnew file mode 100644 index 0000000..18e3607 --- /dev/null +++ b/res/drawable/image_border_bg_normal9.png diff --git a/res/drawable/image_border_bg_pressed_blue.9.png b/res/drawable/image_border_bg_pressed_blue.9.png Binary files differnew file mode 100644 index 0000000..94fa74f --- /dev/null +++ b/res/drawable/image_border_bg_pressed_blue.9.png diff --git a/res/drawable/image_border_bg_pressed_blue9.png b/res/drawable/image_border_bg_pressed_blue9.png Binary files differnew file mode 100644 index 0000000..94fa74f --- /dev/null +++ b/res/drawable/image_border_bg_pressed_blue9.png diff --git a/res/drawable/indicator_autocrop.png b/res/drawable/indicator_autocrop.png Binary files differnew file mode 100644 index 0000000..d960b1f --- /dev/null +++ b/res/drawable/indicator_autocrop.png diff --git a/res/drawable/play_video.png b/res/drawable/play_video.png Binary files differnew file mode 100644 index 0000000..0c10731 --- /dev/null +++ b/res/drawable/play_video.png diff --git a/res/drawable/popup_camera_toast.9.png b/res/drawable/popup_camera_toast.9.png Binary files differnew file mode 100755 index 0000000..c1f0c1f --- /dev/null +++ b/res/drawable/popup_camera_toast.9.png 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 Binary files differnew file mode 100644 index 0000000..be0a0d3 --- /dev/null +++ b/res/raw/camera_click.ogg 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 & 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); + } +} |